diff --git a/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt new file mode 100644 index 000000000..884e7c11f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/cfr/SearchWidgetCFR.kt @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.cfr + +import android.app.Dialog +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.view.marginTop +import kotlinx.android.synthetic.main.search_widget_cfr.view.* +import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.drop_down_triangle +import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.pop_up_triangle +import org.mozilla.fenix.R +import org.mozilla.fenix.components.SearchWidgetCreator +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.utils.Settings + +/** + * Displays a CFR above the HomeFragment toolbar that recommends usage / installation of the search widget. + */ +class SearchWidgetCFR( + private val context: Context, + private val getToolbar: () -> View +) { + + // TODO: Based on pref && is in the bucket...? + fun displayIfNecessary() { + if (!context.settings().shouldDisplaySearchWidgetCFR()) { return } + showSearchWidgetCFR() + } + + @Suppress("MagicNumber", "InflateParams") + private fun showSearchWidgetCFR() { + context.settings().incrementSearchWidgetCFRDisplayed() + + val searchWidgetCFRDialog = Dialog(context) + val layout = LayoutInflater.from(context) + .inflate(R.layout.search_widget_cfr, null) + val isBottomToolbar = Settings.getInstance(context).shouldUseBottomToolbar + + layout.drop_down_triangle.isGone = isBottomToolbar + layout.pop_up_triangle.isVisible = isBottomToolbar + + val toolbar = getToolbar() + + val gravity = if (isBottomToolbar) { + Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + } else { + Gravity.CENTER_HORIZONTAL or Gravity.TOP + } + + layout.cfr_neg_button.setOnClickListener { + searchWidgetCFRDialog.dismiss() + context.settings().manuallyDismissSearchWidgetCFR() + } + + layout.cfr_pos_button.setOnClickListener { + //context.components.analytics.metrics.track(Event.) + SearchWidgetCreator.createSearchWidget(context) + searchWidgetCFRDialog.dismiss() + //context.settings().manuallyDismissSearchWidgetCFR() + } + + searchWidgetCFRDialog.apply { + setContentView(layout) + } + + searchWidgetCFRDialog.window?.let { + it.setGravity(gravity) + val attr = it.attributes + attr.y = + (toolbar.y + toolbar.height - toolbar.marginTop - toolbar.paddingTop).toInt() + it.attributes = attr + it.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + searchWidgetCFRDialog.setOnDismissListener { + context.settings().incrementSearchWidgetCFRDismissed() + } + + searchWidgetCFRDialog.show() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/SearchWidgetCreator.kt b/app/src/main/java/org/mozilla/fenix/components/SearchWidgetCreator.kt new file mode 100644 index 000000000..f16cc4eef --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/SearchWidgetCreator.kt @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components + +import android.annotation.TargetApi +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.os.Build +import org.mozilla.gecko.search.SearchWidgetProvider + +/** + * Handles the creation of search widget. + */ +object SearchWidgetCreator { + + /** + * Attempts to display a prompt requesting the user pin the search widget + * Returns true if the prompt is displayed successfully, and false otherwise. + */ + @TargetApi(Build.VERSION_CODES.O) + fun createSearchWidget(context: Context): Boolean { + val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java) + if (!appWidgetManager.isRequestPinAppWidgetSupported) { return false } + + val myProvider = ComponentName(context, SearchWidgetProvider::class.java) + appWidgetManager.requestPinAppWidget(myProvider, null, null) + + return true + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 8d3f9461c..ccaf1c4b4 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -26,6 +26,7 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat +import androidx.core.view.doOnLayout import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment @@ -78,6 +79,7 @@ import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.collections.SaveCollectionStep +import org.mozilla.fenix.cfr.SearchWidgetCFR import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.StoreProvider @@ -380,6 +382,14 @@ class HomeFragment : Fragment() { ) } } + + // We call this onLayout so that the bottom bar width is correctly set for us to center + // the CFR in. + view.toolbar_wrapper.doOnLayout { + if (!browsingModeManager.mode.isPrivate) { + SearchWidgetCFR(view.context) { view.toolbar_wrapper }.displayIfNecessary() + } + } } override fun onDestroyView() { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt index c69515c53..cbe131f1c 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt @@ -81,6 +81,8 @@ class DefaultSearchController( val event = if (url.isUrl()) { Event.EnteredUrl(false) } else { + context.settings().incrementActiveSearchCount() + val searchAccessPoint = when (store.state.searchAccessPoint) { NONE -> ACTION else -> store.state.searchAccessPoint @@ -142,6 +144,8 @@ class DefaultSearchController( } override fun handleSearchTermsTapped(searchTerms: String) { + context.settings().incrementActiveSearchCount() + activity.openToBrowserAndLoad( searchTermOrURL = searchTerms, newTab = store.state.session == null, diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index f63b874c0..342e15e5e 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -149,6 +149,63 @@ class Settings private constructor( preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_firefox_nightly_tip), true) && preferences.getBoolean(appContext.getString(R.string.pref_key_migrating_from_fenix_tip), true) + private val activeSearchCount by intPreference( + appContext.getPreferenceKey(R.string.pref_key_search_count), + default = 0 + ) + + fun incrementActiveSearchCount() { + preferences.edit().putInt( + appContext.getPreferenceKey(R.string.pref_key_search_count), + activeSearchCount + 1 + ).apply() + } + + private val isActiveSearcher: Boolean + get() = activeSearchCount > 2 + + fun shouldDisplaySearchWidgetCFR(): Boolean = + isActiveSearcher && + searchWidgetCFRDismissCount < 3 && + !searchWidgetInstalled && + !searchWidgetCFRManuallyDismissed + + private val searchWidgetCFRDisplayCount by intPreference( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_display_count), + default = 0 + ) + + fun incrementSearchWidgetCFRDisplayed() { + preferences.edit().putInt( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_display_count), + searchWidgetCFRDisplayCount + 1 + ).apply() + } + + private val searchWidgetCFRManuallyDismissed by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_manually_dismissed), + default = false + ) + + fun manuallyDismissSearchWidgetCFR() { + preferences.edit().putBoolean( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_manually_dismissed), + true + ).apply() + } + + private val searchWidgetCFRDismissCount by intPreference( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_dismiss_count), + default = 0 + ) + + fun incrementSearchWidgetCFRDismissed() { + preferences.edit().putInt( + appContext.getPreferenceKey(R.string.pref_key_search_widget_cfr_dismiss_count), + searchWidgetCFRDismissCount + 1 + ).apply() + } + var defaultSearchEngineName by stringPreference( appContext.getPreferenceKey(R.string.pref_key_search_engine), default = "" diff --git a/app/src/main/res/drawable-xhdpi/search_widget_illustration.png b/app/src/main/res/drawable-xhdpi/search_widget_illustration.png new file mode 100644 index 000000000..dc6539813 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/search_widget_illustration.png differ diff --git a/app/src/main/res/layout/search_widget_cfr.xml b/app/src/main/res/layout/search_widget_cfr.xml new file mode 100644 index 000000000..e5f6be05b --- /dev/null +++ b/app/src/main/res/layout/search_widget_cfr.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + +