From 3ba6593f986c483fccbdd0158268d8374b081aa1 Mon Sep 17 00:00:00 2001 From: Lina Butler Date: Sun, 27 Aug 2023 19:42:40 -0400 Subject: [PATCH] Bug 1851268 - Show Firefox Suggest search suggestions in Fenix. This commit integrates the Firefox Suggest Android component added in bug 1850296 into Fenix, and adds: * A Nimbus feature for Firefox Suggest. * A secret setting to enable the Firefox Suggest feature, only visible on the debug channel. * Search settings for toggling sponsored and non-sponsored suggestions, only visible when the Firefox Suggest feature is enabled. When the feature is enabled, Fenix will ingest new suggestions in the background, show the new Search settings, and show matching suggestions in the awesomebar depending on those Search settings. --- .buildconfig.yml | 1 + app/.experimenter.yaml | 8 ++++ app/build.gradle | 1 + app/nimbus.fml.yaml | 18 ++++++++ .../java/org/mozilla/fenix/FeatureFlags.kt | 5 +++ .../org/mozilla/fenix/FenixApplication.kt | 11 +++++ .../mozilla/fenix/components/Components.kt | 2 + .../org/mozilla/fenix/components/FxSuggest.kt | 25 +++++++++++ .../fenix/search/awesomebar/AwesomeBarView.kt | 16 +++++++ .../fenix/settings/SecretSettingsFragment.kt | 21 ++++++++++ .../settings/search/SearchEngineFragment.kt | 28 +++++++++++++ .../java/org/mozilla/fenix/utils/Settings.kt | 30 +++++++++++++ app/src/main/res/values/preference_keys.xml | 3 ++ app/src/main/res/values/static_strings.xml | 2 + app/src/main/res/values/strings.xml | 10 +++++ .../res/xml/search_settings_preferences.xml | 10 +++++ .../res/xml/secret_settings_preferences.xml | 5 +++ .../search/awesomebar/AwesomeBarViewTest.kt | 42 +++++++++++++++++++ .../search/SearchEngineFragmentTest.kt | 10 +++++ 19 files changed, 248 insertions(+) create mode 100644 app/src/main/java/org/mozilla/fenix/components/FxSuggest.kt diff --git a/.buildconfig.yml b/.buildconfig.yml index 808a4246c..8d4747695 100644 --- a/.buildconfig.yml +++ b/.buildconfig.yml @@ -35,6 +35,7 @@ projects: - feature-customtabs - feature-downloads - feature-findinpage + - feature-fxsuggest - feature-intent - feature-logins - feature-media diff --git a/app/.experimenter.yaml b/app/.experimenter.yaml index 1468a8831..6689c7550 100644 --- a/app/.experimenter.yaml +++ b/app/.experimenter.yaml @@ -15,6 +15,14 @@ extensions-process: enabled: type: boolean description: "If true, the extensions process is enabled." +fx-suggest: + description: A feature that provides Firefox Suggest search suggestions. + hasExposure: true + exposureDescription: "" + variables: + enabled: + type: boolean + description: "Whether the feature is enabled. When Firefox Suggest is enabled, Firefox will download and store new search suggestions in the background, and show additional Search settings to control which suggestions appear in the awesomebar. When Firefox Suggest is disabled, Firefox will not download new suggestions, and hide the additional Search settings.\n" glean: description: A feature that provides server-side configurations for Glean metrics (aka Server Knobs). hasExposure: true diff --git a/app/build.gradle b/app/build.gradle index 16a8698a3..27e805a2b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -517,6 +517,7 @@ dependencies { implementation project(':feature-contextmenu') implementation project(':feature-customtabs') implementation project(':feature-downloads') + implementation project(':feature-fxsuggest') implementation project(':feature-intent') implementation project(':feature-media') implementation project(':feature-prompts') diff --git a/app/nimbus.fml.yaml b/app/nimbus.fml.yaml index 2147af07f..3481a8b6d 100644 --- a/app/nimbus.fml.yaml +++ b/app/nimbus.fml.yaml @@ -386,6 +386,24 @@ features: description: The channel Id param name with arg. type: Map default: {} + + fx-suggest: + description: A feature that provides Firefox Suggest search suggestions. + variables: + enabled: + description: > + Whether the feature is enabled. When Firefox Suggest is enabled, + Firefox will download and store new search suggestions in the + background, and show additional Search settings to control which + suggestions appear in the awesomebar. When Firefox Suggest is + disabled, Firefox will not download new suggestions, and hide the + additional Search settings. + type: Boolean + default: false + defaults: + - channel: developer + value: + enabled: true types: objects: {} diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 96da99a8f..2922a6dff 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -74,4 +74,9 @@ object FeatureFlags { * Preference to fully enable translations is pref_key_enable_translations. */ val translations = Config.channel.isDebug + + /** + * Allows users to enable Firefox Suggest. + */ + val fxSuggest = Config.channel.isDebug } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index ea2859cec..938af48c6 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -43,6 +43,7 @@ import mozilla.components.concept.storage.FrecencyThresholdOption import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.feature.autofill.AutofillUseCases +import mozilla.components.feature.fxsuggest.GlobalFxSuggestDependencyProvider import mozilla.components.feature.search.ext.buildSearchUrl import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine import mozilla.components.feature.top.sites.TopSitesFrecencyConfig @@ -282,6 +283,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider { startMetricsIfEnabled() setupPush() + GlobalFxSuggestDependencyProvider.initialize(components.fxSuggest.storage) + visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService()) registerActivityLifecycleCallbacks(visibilityLifecycleCallback) registerActivityLifecycleCallbacks(MarkersActivityLifecycleCallbacks(components.core.engine)) @@ -365,6 +368,14 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.core.historyMetadataService.cleanup( System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS, ) + + // If Firefox Suggest is enabled, register a worker to periodically ingest + // new search suggestions. The worker requires us to have called + // `GlobalFxSuggestDependencyProvider.initialize`, which we did before + // scheduling these tasks. + if (settings().enableFxSuggest) { + components.fxSuggest.ingestionScheduler.startPeriodicIngestion() + } } } // Account manager initialization needs to happen on the main thread. diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 23db1ec60..fb5dc1072 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -226,6 +226,8 @@ class Components(private val context: Context) { ), ) } + + val fxSuggest by lazyMonitored { FxSuggest(context) } } /** diff --git a/app/src/main/java/org/mozilla/fenix/components/FxSuggest.kt b/app/src/main/java/org/mozilla/fenix/components/FxSuggest.kt new file mode 100644 index 000000000..05593c97f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/FxSuggest.kt @@ -0,0 +1,25 @@ +/* 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.content.Context +import mozilla.components.feature.fxsuggest.FxSuggestIngestionScheduler +import mozilla.components.feature.fxsuggest.FxSuggestStorage +import org.mozilla.fenix.perf.lazyMonitored + +/** + * Component group for Firefox Suggest. + * + * @param context The Android application context. + */ +class FxSuggest(context: Context) { + val storage by lazyMonitored { + FxSuggestStorage(context) + } + + val ingestionScheduler by lazyMonitored { + FxSuggestIngestionScheduler(context) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index 197098e9c..4455fe02c 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -25,6 +25,7 @@ import mozilla.components.feature.awesomebar.provider.SearchEngineSuggestionProv import mozilla.components.feature.awesomebar.provider.SearchSuggestionProvider import mozilla.components.feature.awesomebar.provider.SearchTermSuggestionsProvider import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider +import mozilla.components.feature.fxsuggest.FxSuggestSuggestionProvider import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.syncedtabs.DeviceIndicators @@ -56,6 +57,7 @@ class AwesomeBarView( private val engineForSpeculativeConnects: Engine? private val defaultHistoryStorageProvider: HistoryStorageSuggestionProvider private val defaultCombinedHistoryProvider: CombinedHistorySuggestionProvider + private val fxSuggestProvider: FxSuggestSuggestionProvider? private val shortcutsEnginePickerProvider: ShortcutsSuggestionProvider private val defaultSearchSuggestionProvider: SearchSuggestionProvider private val defaultSearchActionProvider: SearchActionProvider @@ -138,6 +140,18 @@ class AwesomeBarView( suggestionsHeader = activity.getString(R.string.firefox_suggest_header), ) + fxSuggestProvider = if (activity.settings().enableFxSuggest) { + FxSuggestSuggestionProvider( + resources = activity.resources, + loadUrlUseCase = loadUrlUseCase, + includeSponsoredSuggestions = activity.settings().showSponsoredSuggestions, + includeNonSponsoredSuggestions = activity.settings().showNonSponsoredSuggestions, + suggestionsHeader = activity.getString(R.string.firefox_suggest_header), + ) + } else { + null + } + val searchBitmap = getDrawable(activity, R.drawable.ic_search)!!.apply { colorFilter = createBlendModeColorFilterCompat(primaryTextColor, SRC_IN) }.toBitmap() @@ -320,6 +334,8 @@ class AwesomeBarView( providersToAdd.add(searchEngineSuggestionProvider) + fxSuggestProvider?.let { providersToAdd.add(it) } + return providersToAdd } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt index a1753f7a7..17f625be0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.settings import android.os.Bundle +import androidx.core.content.edit import androidx.navigation.fragment.findNavController import androidx.preference.EditTextPreference import androidx.preference.Preference @@ -71,6 +72,26 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { onPreferenceChangeListener = SharedPreferenceUpdater() } + requirePreference(R.string.pref_key_enable_fxsuggest).apply { + isVisible = FeatureFlags.fxSuggest + isChecked = context.settings().enableFxSuggest + onPreferenceChangeListener = object : Preference.OnPreferenceChangeListener { + override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { + val newBooleanValue = newValue as? Boolean ?: return false + val ingestionScheduler = requireContext().components.fxSuggest.ingestionScheduler + if (newBooleanValue) { + ingestionScheduler.startPeriodicIngestion() + } else { + ingestionScheduler.stopPeriodicIngestion() + } + requireContext().settings().preferences.edit { + putBoolean(preference.key, newBooleanValue) + } + return true + } + } + } + // for performance reasons, this is only available in Nightly or Debug builds requirePreference(R.string.pref_key_custom_glean_server_url).apply { isVisible = Config.channel.isNightlyOrDebug && BuildConfig.GLEAN_CUSTOM_URL.isNullOrEmpty() diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt index 683f77d3e..acbdbffa2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt @@ -38,10 +38,17 @@ class SearchEngineFragment : PreferenceFragmentCompat() { requirePreference(R.string.pref_key_show_search_engine_shortcuts).apply { isVisible = !context.settings().showUnifiedSearchFeature } + requirePreference(R.string.pref_key_show_sponsored_suggestions).apply { + isVisible = context.settings().enableFxSuggest + } + requirePreference(R.string.pref_key_show_nonsponsored_suggestions).apply { + isVisible = context.settings().enableFxSuggest + } view?.hideKeyboard() } + @Suppress("LongMethod") override fun onResume() { super.onResume() view?.hideKeyboard() @@ -99,6 +106,24 @@ class SearchEngineFragment : PreferenceFragmentCompat() { isChecked = context.settings().shouldShowVoiceSearch } + val showSponsoredSuggestionsPreference = + requirePreference(R.string.pref_key_show_sponsored_suggestions).apply { + isChecked = context.settings().showSponsoredSuggestions + summary = getString( + R.string.preferences_show_sponsored_suggestions_summary, + getString(R.string.app_name), + ) + } + + val showNonSponsoredSuggestionsPreference = + requirePreference(R.string.pref_key_show_nonsponsored_suggestions).apply { + isChecked = context.settings().showNonSponsoredSuggestions + title = getString( + R.string.preferences_show_nonsponsored_suggestions, + getString(R.string.app_name), + ) + } + searchSuggestionsPreference.onPreferenceChangeListener = SharedPreferenceUpdater() showSearchShortcuts.onPreferenceChangeListener = SharedPreferenceUpdater() showHistorySuggestions.onPreferenceChangeListener = SharedPreferenceUpdater() @@ -125,6 +150,9 @@ class SearchEngineFragment : PreferenceFragmentCompat() { } true } + + showSponsoredSuggestionsPreference.onPreferenceChangeListener = SharedPreferenceUpdater() + showNonSponsoredSuggestionsPreference.onPreferenceChangeListener = SharedPreferenceUpdater() } override fun onPreferenceTreeClick(preference: Preference): Boolean { 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 3898f216f..884323025 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -1863,4 +1863,34 @@ class Settings(private val appContext: Context) : PreferencesHolder { appContext.getPreferenceKey(R.string.pref_key_hidden_engines_restored), default = false, ) + + /** + * Indicates if Firefox Suggest is enabled. + */ + var enableFxSuggest by lazyFeatureFlagPreference( + key = appContext.getPreferenceKey(R.string.pref_key_enable_fxsuggest), + default = { FxNimbus.features.fxSuggest.value().enabled }, + featureFlag = FeatureFlags.fxSuggest, + ) + + /** + * Indicates if the user has chosen to show sponsored search suggestions in the awesomebar. + * The default value is computed lazily, and based on whether Firefox Suggest is enabled. + */ + var showSponsoredSuggestions by lazyFeatureFlagPreference( + key = appContext.getPreferenceKey(R.string.pref_key_show_sponsored_suggestions), + default = { enableFxSuggest }, + featureFlag = FeatureFlags.fxSuggest, + ) + + /** + * Indicates if the user has chosen to show search suggestions for web content in the + * awesomebar. The default value is computed lazily, and based on whether Firefox Suggest + * is enabled. + */ + var showNonSponsoredSuggestions by lazyFeatureFlagPreference( + key = appContext.getPreferenceKey(R.string.pref_key_show_nonsponsored_suggestions), + default = { enableFxSuggest }, + featureFlag = FeatureFlags.fxSuggest, + ) } diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index cf367c7cb..26ea06ae5 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -76,6 +76,7 @@ pref_key_nimbus_last_fetch pref_key_home_blocklist pref_key_hidden_engines_restored + pref_key_fxsuggest_enabled pref_key_telemetry @@ -113,6 +114,8 @@ pref_key_show_search_suggestions_in_privateonboarding pref_key_show_voice_search pref_key_enable_domain_autocomplete + pref_key_show_sponsored_suggestions + pref_key_show_nonsponsored_suggestions pref_key_site_permissions_description diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml index dd89e6b8a..ad826c0f9 100644 --- a/app/src/main/res/values/static_strings.xml +++ b/app/src/main/res/values/static_strings.xml @@ -70,6 +70,8 @@ Enable Shopping Experience Enable Firefox Translations + + Enable Firefox Suggest Make inactive diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 67b6df39c..b9f8c5b34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -542,6 +542,16 @@ Account settings Autocomplete URLs + + Suggestions from sponsors + + Support %1$s with occasional sponsored suggestions + + Suggestions from %1$s + + Get suggestions from the web related to your search Open links in apps diff --git a/app/src/main/res/xml/search_settings_preferences.xml b/app/src/main/res/xml/search_settings_preferences.xml index c6cb90fb1..0ecf24204 100644 --- a/app/src/main/res/xml/search_settings_preferences.xml +++ b/app/src/main/res/xml/search_settings_preferences.xml @@ -72,5 +72,15 @@ android:layout="@layout/checkbox_left_preference" android:title="@string/preferences_show_search_suggestions_in_private" app:iconSpaceReserved="false" /> + + diff --git a/app/src/main/res/xml/secret_settings_preferences.xml b/app/src/main/res/xml/secret_settings_preferences.xml index 3c529d2bc..f2eb8c195 100644 --- a/app/src/main/res/xml/secret_settings_preferences.xml +++ b/app/src/main/res/xml/secret_settings_preferences.xml @@ -35,6 +35,11 @@ android:key="@string/pref_key_enable_translations" android:title="@string/preferences_debug_settings_translations" app:iconSpaceReserved="false" /> + ().components.backgroundServices.syncedTabsStorage } returns mockk() every { any().components.core.store.state.search } returns mockk(relaxed = true) every { any().components.core.store.state.search } returns mockk(relaxed = true) + every { any().settings() } returns mockk(relaxed = true) every { any().getColorFromAttr(any()) } returns 0 every { AwesomeBarView.Companion.getDrawable(any(), any()) } returns mockk(relaxed = true) { every { intrinsicWidth } returns 10 @@ -1044,6 +1046,46 @@ class AwesomeBarViewTest { assertEquals(1, result.filterIsInstance().size) } + + @Test + fun `GIVEN Firefox Suggest is enabled WHEN the view is created THEN configure the Firefox Suggest suggestion provider`() { + val settings: Settings = mockk(relaxed = true) { + every { enableFxSuggest } returns true + } + every { activity.settings() } returns settings + val awesomeBarView = AwesomeBarView( + activity = activity, + interactor = mockk(), + view = mockk(), + fromHomeFragment = false, + ) + val state = getSearchProviderState() + + val result = awesomeBarView.getProvidersToAdd(state) + + val fxSuggestProvider = result.firstOrNull { it is FxSuggestSuggestionProvider } + assertNotNull(fxSuggestProvider) + } + + @Test + fun `GIVEN Firefox Suggest is disabled WHEN the view is created THEN don't configure the Firefox Suggest suggestion provider`() { + val settings: Settings = mockk(relaxed = true) { + every { enableFxSuggest } returns false + } + every { activity.settings() } returns settings + val awesomeBarView = AwesomeBarView( + activity = activity, + interactor = mockk(), + view = mockk(), + fromHomeFragment = false, + ) + val state = getSearchProviderState() + + val result = awesomeBarView.getProvidersToAdd(state) + + val fxSuggestProvider = result.firstOrNull { it is FxSuggestSuggestionProvider } + assertNull(fxSuggestProvider) + } } /** diff --git a/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt index c69f4b9fb..236a8d547 100644 --- a/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/settings/search/SearchEngineFragmentTest.kt @@ -89,6 +89,16 @@ class SearchEngineFragmentTest { } returns mockk(relaxed = true) { every { context } returns testContext } + every { + fragment.findPreference(testContext.getString(R.string.pref_key_show_sponsored_suggestions)) + } returns mockk(relaxed = true) { + every { context } returns testContext + } + every { + fragment.findPreference(testContext.getString(R.string.pref_key_show_nonsponsored_suggestions)) + } returns mockk(relaxed = true) { + every { context } returns testContext + } // This preference is the sole purpose of this test every { fragment.findPreference(voiceSearchPreferenceKey)