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.
fenix/119.0
Lina Butler 9 months ago committed by mergify[bot]
parent 924a831c94
commit 3ba6593f98

@ -35,6 +35,7 @@ projects:
- feature-customtabs
- feature-downloads
- feature-findinpage
- feature-fxsuggest
- feature-intent
- feature-logins
- feature-media

@ -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

@ -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')

@ -386,6 +386,24 @@ features:
description: The channel Id param name with arg.
type: Map<String, String>
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: {}

@ -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
}

@ -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.

@ -226,6 +226,8 @@ class Components(private val context: Context) {
),
)
}
val fxSuggest by lazyMonitored { FxSuggest(context) }
}
/**

@ -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)
}
}

@ -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
}

@ -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<SwitchPreference>(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<EditTextPreference>(R.string.pref_key_custom_glean_server_url).apply {
isVisible = Config.channel.isNightlyOrDebug && BuildConfig.GLEAN_CUSTOM_URL.isNullOrEmpty()

@ -38,10 +38,17 @@ class SearchEngineFragment : PreferenceFragmentCompat() {
requirePreference<SwitchPreference>(R.string.pref_key_show_search_engine_shortcuts).apply {
isVisible = !context.settings().showUnifiedSearchFeature
}
requirePreference<SwitchPreference>(R.string.pref_key_show_sponsored_suggestions).apply {
isVisible = context.settings().enableFxSuggest
}
requirePreference<SwitchPreference>(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<SwitchPreference>(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<SwitchPreference>(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 {

@ -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,
)
}

@ -76,6 +76,7 @@
<string name="pref_key_nimbus_last_fetch" translatable="false">pref_key_nimbus_last_fetch</string>
<string name="pref_key_home_blocklist">pref_key_home_blocklist</string>
<string name="pref_key_hidden_engines_restored" translatable="false">pref_key_hidden_engines_restored</string>
<string name="pref_key_enable_fxsuggest" translatable="false">pref_key_fxsuggest_enabled</string>
<!-- Data Choices -->
<string name="pref_key_telemetry" translatable="false">pref_key_telemetry</string>
@ -113,6 +114,8 @@
<string name="pref_key_show_search_suggestions_in_private_onboarding" translatable="false">pref_key_show_search_suggestions_in_privateonboarding</string>
<string name="pref_key_show_voice_search" translatable="false">pref_key_show_voice_search</string>
<string name="pref_key_enable_autocomplete_urls" translatable="false">pref_key_enable_domain_autocomplete</string>
<string name="pref_key_show_sponsored_suggestions" translatable="false">pref_key_show_sponsored_suggestions</string>
<string name="pref_key_show_nonsponsored_suggestions" translatable="false">pref_key_show_nonsponsored_suggestions</string>
<!-- Site Permissions Settings -->
<string name="pref_key_site_permissions_description" translatable="false">pref_key_site_permissions_description</string>

@ -70,6 +70,8 @@
<string name="preferences_debug_settings_shopping_experience" translatable="false">Enable Shopping Experience</string>
<!-- Label for enabling translations -->
<string name="preferences_debug_settings_translations" translatable="false">Enable Firefox Translations</string>
<!-- Label for enabling Firefox Suggest -->
<string name="preferences_debug_settings_fxsuggest" translatable="false">Enable Firefox Suggest</string>
<!-- A secret menu option in the tabs tray for making a tab inactive for testing. -->
<string name="inactive_tabs_menu_item">Make inactive</string>

@ -542,6 +542,16 @@
<string name="preferences_account_settings">Account settings</string>
<!-- Preference for enabling url autocomplete-->
<string name="preferences_enable_autocomplete_urls">Autocomplete URLs</string>
<!-- Preference title for switch preference to show sponsored Firefox Suggest search suggestions -->
<string name="preferences_show_sponsored_suggestions">Suggestions from sponsors</string>
<!-- Summary for preference to show sponsored Firefox Suggest search suggestions.
The first parameter is the name of the application. -->
<string name="preferences_show_sponsored_suggestions_summary">Support %1$s with occasional sponsored suggestions</string>
<!-- Preference title for switch preference to show Firefox Suggest search suggestions for web content.
The first parameter is the name of the application. -->
<string name="preferences_show_nonsponsored_suggestions">Suggestions from %1$s</string>
<!-- Summary for preference to show Firefox Suggest search suggestions for web content -->
<string name="preferences_show_nonsponsored_suggestions_summary">Get suggestions from the web related to your search</string>
<!-- Preference for open links in third party apps -->
<string name="preferences_open_links_in_apps">Open links in apps</string>
<!-- Preference for open links in third party apps always open in apps option -->

@ -72,5 +72,15 @@
android:layout="@layout/checkbox_left_preference"
android:title="@string/preferences_show_search_suggestions_in_private"
app:iconSpaceReserved="false" />
<SwitchPreference
app:iconSpaceReserved="false"
android:key="@string/pref_key_show_nonsponsored_suggestions"
android:title="@string/preferences_show_nonsponsored_suggestions"
android:summary="@string/preferences_show_nonsponsored_suggestions_summary" />
<SwitchPreference
app:iconSpaceReserved="false"
android:key="@string/pref_key_show_sponsored_suggestions"
android:title="@string/preferences_show_sponsored_suggestions"
android:summary="@string/preferences_show_sponsored_suggestions_summary" />
</PreferenceCategory>
</PreferenceScreen>

@ -35,6 +35,11 @@
android:key="@string/pref_key_enable_translations"
android:title="@string/preferences_debug_settings_translations"
app:iconSpaceReserved="false" />
<SwitchPreference
android:defaultValue="false"
android:key="@string/pref_key_enable_fxsuggest"
android:title="@string/preferences_debug_settings_fxsuggest"
app:iconSpaceReserved="false" />
<EditTextPreference
android:key="@string/pref_key_custom_glean_server_url"
android:title="@string/preferences_debug_settings_custom_glean_server_url"

@ -24,6 +24,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.syncedtabs.SyncedTabsStorageSuggestionProvider
import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.junit.After
@ -65,6 +66,7 @@ class AwesomeBarViewTest {
every { any<Activity>().components.backgroundServices.syncedTabsStorage } returns mockk()
every { any<Activity>().components.core.store.state.search } returns mockk(relaxed = true)
every { any<Activity>().components.core.store.state.search } returns mockk(relaxed = true)
every { any<Activity>().settings() } returns mockk(relaxed = true)
every { any<Activity>().getColorFromAttr(any()) } returns 0
every { AwesomeBarView.Companion.getDrawable(any(), any()) } returns mockk<VectorDrawable>(relaxed = true) {
every { intrinsicWidth } returns 10
@ -1044,6 +1046,46 @@ class AwesomeBarViewTest {
assertEquals(1, result.filterIsInstance<SearchTermSuggestionsProvider>().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)
}
}
/**

@ -89,6 +89,16 @@ class SearchEngineFragmentTest {
} returns mockk(relaxed = true) {
every { context } returns testContext
}
every {
fragment.findPreference<SwitchPreference>(testContext.getString(R.string.pref_key_show_sponsored_suggestions))
} returns mockk(relaxed = true) {
every { context } returns testContext
}
every {
fragment.findPreference<SwitchPreference>(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<SwitchPreference>(voiceSearchPreferenceKey)

Loading…
Cancel
Save