First use of Nimbus FML plugin (#23400)

* Consume Nimbus FML plugin

* Convert Homescreen to use FML

* Convert nimbusValidation to use FML

* Convert legacy experiments to use the feature API and FML

Remove dead helper code and documentation

* Fixup failing test

Co-authored-by: Grisha Kruglov <gkruglov@mozilla.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
upstream-sync
jhugman 2 years ago committed by GitHub
parent e92fe26df7
commit 82a6f8cae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,59 @@
{
"search-term-groups": {
"description": "A feature allowing the grouping of URLs around the search term that it came from.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"enabled": {
"description": "If true, the feature shows up on the homescreen and on the new tab screen.",
"type": "boolean"
}
}
},
"default-browser-message": {
"description": "A small feature allowing experiments on the placement of a default browser message.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"message-location": {
"description": "Where is the message to be put.",
"enum": [
"homescreen-banner",
"settings",
"app-menu-item"
],
"type": "string"
}
}
},
"nimbus-validation": {
"description": "A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"settings-icon": {
"description": "The drawable displayed in the app menu for Settings",
"type": "string"
},
"settings-punctuation": {
"description": "The emoji displayed in the Settings screen title.",
"type": "string"
},
"settings-title": {
"description": "The title of displayed in the Settings screen and app menu.",
"type": "string"
}
}
},
"homescreen": {
"description": "The homescreen that the user goes to when they press home or new tab.",
"hasExposure": true,
"exposureDescription": "",
"variables": {
"sections-enabled": {
"description": "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default.",
"type": "json"
}
}
}
}

@ -380,7 +380,28 @@ android.applicationVariants.all { variant ->
// Generate Kotlin code for the Fenix Glean metrics.
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
apply plugin: "org.mozilla.components.nimbus-gradle-plugin"
nimbus {
// The path to the Nimbus feature manifest file
manifestFile = "nimbus.fml.yaml"
// The fully qualified class name for the generated features.
// If the classname begins with a '.' this is taken as a suffix to the app's package name
destinationClass = ".nimbus.FxNimbus"
// Map from the variant name to the channel as experimenter and nimbus understand it.
// If nimbus's channels were accurately set up well for this project, then this
// shouldn't be needed.
channels = [
debug: "developer",
nightly: "nightly",
beta: "beta",
release: "release",
]
// This is generated by the FML and should be checked into git.
// It will be fetched by Experimenter (the Nimbus experiment website)
// and used to inform experiment configuration.
experimenterManifest = ".experimenter.json"
}
configurations {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in

@ -66,7 +66,6 @@ import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.isCustomEngine
import org.mozilla.fenix.ext.isKnownSearchDomain
@ -273,8 +272,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS
)
}
SecurePrefsTelemetry(this@FenixApplication, components.analytics.experiments).startTests()
}
// Account manager initialization needs to happen on the main thread.
GlobalScope.launch(Dispatchers.Main) {

@ -23,9 +23,9 @@ import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.NimbusFeatures
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
@ -109,11 +109,9 @@ class Analytics(
}
val experiments: NimbusApi by lazyMonitored {
createNimbus(context, BuildConfig.NIMBUS_ENDPOINT)
}
val features: NimbusFeatures by lazyMonitored {
NimbusFeatures(context)
createNimbus(context, BuildConfig.NIMBUS_ENDPOINT).also { api ->
FxNimbus.api = api
}
}
}

@ -1,35 +0,0 @@
/* 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.metrics
import android.content.Context
import android.os.Build
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
import mozilla.components.service.nimbus.NimbusApi
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.withExperiment
/**
* Allows starting a quick test of ACs SecureAbove22Preferences that will emit Facts
* for the basic operations and allow us to log them for later evaluation of APIs stability.
*/
class SecurePrefsTelemetry(
private val appContext: Context,
private val experiments: NimbusApi
) {
fun startTests() {
// The Android Keystore is used to secure the shared prefs only on API 23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// These tests should run only if the experiment is live
experiments.withExperiment(FeatureId.ANDROID_KEYSTORE) { experimentBranch ->
// .. and this device is not in the control group.
if (experimentBranch == ExperimentBranch.TREATMENT) {
SecurePrefsReliabilityExperiment(appContext)()
}
}
}
}
}

@ -37,11 +37,10 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
@ -361,6 +360,7 @@ open class DefaultToolbarMenu(
@VisibleForTesting(otherwise = PRIVATE)
val coreMenuItems by lazy {
val defaultBrowserItem = getSetDefaultBrowserItem()
val menuItems =
listOfNotNull(
if (shouldUseBottomToolbar) null else menuToolbar,
@ -372,8 +372,8 @@ open class DefaultToolbarMenu(
extensionsItem,
syncMenuItem,
BrowserMenuDivider(),
getSetDefaultBrowserItem(),
getSetDefaultBrowserItem()?.let { BrowserMenuDivider() },
defaultBrowserItem,
defaultBrowserItem?.let { BrowserMenuDivider() },
findInPageItem,
desktopSiteItem,
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },
@ -446,23 +446,17 @@ open class DefaultToolbarMenu(
}
}
private fun getSetDefaultBrowserItem(): BrowserMenuImageText? {
val experiments = context.components.analytics.experiments
val browsers = BrowsersCache.all(context)
return experiments.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch ->
if (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_TOOLBAR_MENU &&
!browsers.isFirefoxDefaultBrowser
private fun getSetDefaultBrowserItem(): BrowserMenuImageText? =
if (BrowsersCache.all(context).isFirefoxDefaultBrowser) {
null
} else if (FxNimbus.features.defaultBrowserMessage.value().messageLocation == MessageSurfaceId.APP_MENU_ITEM) {
BrowserMenuImageText(
label = context.getString(R.string.preferences_set_as_default_browser),
imageResource = R.mipmap.ic_launcher
) {
return@withExperiment BrowserMenuImageText(
label = context.getString(R.string.preferences_set_as_default_browser),
imageResource = R.mipmap.ic_launcher
) {
onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
}
} else {
null
onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
}
} else {
null
}
}
}

@ -1,35 +0,0 @@
/* 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.experiments
/**
* Enums to identify features in the app. These will likely grow and shrink depending
* on the experiments we want to perform.
*
* @property jsonName the kebab-case version of the feature id as represented in the Nimbus
* experiment JSON.
*/
enum class FeatureId(val jsonName: String) {
NIMBUS_VALIDATION("nimbus-validation"),
ANDROID_KEYSTORE("fenix-android-keystore"),
DEFAULT_BROWSER("fenix-default-browser"),
HOME_PAGE("homescreen")
}
/**
* Experiment branches are becoming less interesting, though we collect some well
* defined ones here.
*/
class ExperimentBranch {
companion object {
const val TREATMENT = "treatment"
const val CONTROL = "control"
const val A1 = "a1"
const val A2 = "a2"
const val DEFAULT_BROWSER_TOOLBAR_MENU = "default_browser_toolbar_menu"
const val DEFAULT_BROWSER_NEW_TAB_BANNER = "default_browser_newtab_banner"
const val DEFAULT_BROWSER_SETTINGS_MENU = "default_browser_settings_menu"
}
}

@ -1,119 +0,0 @@
/* 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.experiments
import android.content.Context
import org.mozilla.experiments.nimbus.mapKeysAsEnums
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getVariables
/**
* Component for exposing nimbus Feature Variables.
* For more information see https://experimenter.info/feature-variables-and-me
*
* @param context - A [Context] for accessing the feature variables from nimbus.
*/
class NimbusFeatures(private val context: Context) {
val homeScreen: HomeScreenFeatures by lazy {
HomeScreenFeatures(context)
}
/**
* Component that indicates which features should be active on the home screen.
*/
class HomeScreenFeatures(private val context: Context) {
/**
* `FeatureId.HOME_PAGE` feature; the complete JSON, is shown here:
*
* ```json
* {
* "sections-enabled": {
* "topSites": true,
* "recentlySaved": false,
* "jumpBackIn": false,
* "pocket": false,
* "recentExplorations": false
* }
* }
* ```
*/
/**
* This enum accompanies the `FeatureId.HOME_PAGE` feature.
*
* These names here should match the names of entries in the JSON.
*/
@Suppress("EnumNaming")
private enum class HomeScreenSection(val default: Boolean) {
topSites(true),
recentlySaved(true),
jumpBackIn(true),
pocket(true),
recentExplorations(true);
companion object {
/**
* CreateS a map with the corresponding default values for each sections.
*/
fun toMap(context: Context): Map<HomeScreenSection, Boolean> {
return values().associate { section ->
val value = if (section == pocket) {
FeatureFlags.isPocketRecommendationsFeatureEnabled(context)
} else {
section.default
}
section to value
}
}
}
}
private val homeScreenFeatures: Map<HomeScreenSection, Boolean> by lazy {
val experiments = context.components.analytics.experiments
val variables = experiments.getVariables(FeatureId.HOME_PAGE, false)
val sections: Map<HomeScreenSection, Boolean> =
variables.getBoolMap("sections-enabled")?.mapKeysAsEnums()
?: HomeScreenSection.toMap(context)
sections
}
/**
* Indicates if the recently tabs feature is active.
*/
fun isRecentlyTabsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.jumpBackIn] == true
}
/**
* Indicates if the recently saved feature is active.
*/
fun isRecentlySavedActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.recentlySaved] == true
}
/**
* Indicates if the recently exploration feature is active.
*/
fun isRecentExplorationsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.recentExplorations] == true
}
/**
* Indicates if the pocket recommendations feature is active.
*/
fun isPocketRecommendationsActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.pocket] == true
}
/**
* Indicates if the top sites feature is active.
*/
fun isTopSitesActive(): Boolean {
return homeScreenFeatures[HomeScreenSection.topSites] == true
}
}
}

@ -1,110 +0,0 @@
/* 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.ext
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.Variables
import org.mozilla.fenix.experiments.FeatureId
/**
* Gets the branch name of an experiment acting on the feature given `featureId`, and transforms it
* with given closure.
*
* You are probably looking for `withVariables`.
*
* If we're enrolled in an experiment, the transform is passed the branch id/slug as a `String`.
*
* If we're not enrolled in the experiment, or the experiment is not valid then the transform
* is passed a `null`.
*/
fun <T> NimbusApi.withExperiment(featureId: FeatureId, transform: (String?) -> T): T {
return transform(withExperiment(featureId))
}
/**
* The synonym for [getExperimentBranch] to complement [withExperiment(String, (String?) -> T))].
*
* Short-hand for ` org.mozilla.experiments.nimbus.NimbusApi.getExperimentBranch`.
*/
@Suppress("TooGenericExceptionCaught")
fun NimbusApi.withExperiment(featureId: FeatureId) =
try {
getExperimentBranch(featureId.jsonName)
} catch (e: Throwable) {
Logger.error("Failed to getExperimentBranch(${featureId.jsonName})", e)
null
}
/**
* Get the variables needed to configure the feature given by `featureId`.
*
* @param featureId The feature id that identifies the feature under experiment.
*
* @param sendExposureEvent Passing `true` to this parameter will record the exposure event
* automatically if the client is enrolled in an experiment for the given [featureId].
* Passing `false` here indicates that the application will manually record the exposure
* event by calling the `sendExposureEvent` function at the time of the exposure to the
* feature.
*
* See [sendExposureEvent] for more information on manually recording the event.
*
* @return a [Variables] object used to configure the feature.
*/
fun NimbusApi.getVariables(featureId: FeatureId, sendExposureEvent: Boolean = true) =
getVariables(featureId.jsonName, sendExposureEvent)
/**
* A synonym for `getVariables(featureId, sendExposureEvent)`.
*
* This exists as a complement to the `withVariable(featureId, sendExposureEvent, transform)` method.
*
* @param featureId the id of the feature as it appears in `Experimenter`
* @param sendExposureEvent by default `true`. This logs an event that the user was exposed to an experiment
* involving this feature.
* @return a `Variables` object providing typed accessors to a remotely configured JSON object.
*/
fun NimbusApi.withVariables(featureId: FeatureId, sendExposureEvent: Boolean = true) =
getVariables(featureId, sendExposureEvent)
/**
* Get a `Variables` object for this feature and use that to configure the feature itself or a
* more type safe configuration object.
*
* @param featureId the id of the feature as it appears in `Experimenter`
* @param sendExposureEvent by default `true`. This logs an event that the user was exposed to an experiment
* involving this feature.
*/
fun <T> NimbusApi.withVariables(featureId: FeatureId, sendExposureEvent: Boolean = true, transform: (Variables) -> T) =
transform(getVariables(featureId, sendExposureEvent))
/**
* Records the `exposure` event in telemetry.
*
* This is a manual function to accomplish the same purpose as passing `true` as the
* `sendExposureEvent` property of the `getVariables` function. It is intended to be used
* when requesting feature variables must occur at a different time than the actual user's
* exposure to the feature within the app.
*
* - Examples:
* - If the `Variables` are needed at a different time than when the exposure to the feature
* actually happens, such as constructing a menu happening at a different time than the
* user seeing the menu.
* - If `getVariables` is required to be called multiple times for the same feature and it is
* desired to only record the exposure once, such as if `getVariables` were called
* with every keystroke.
*
* In the case where the use of this function is required, then the `getVariables` function
* should be called with `false` so that the exposure event is not recorded when the variables
* are fetched.
*
* This function is safe to call even when there is no active experiment for the feature. The SDK
* will ensure that an event is only recorded for active experiments.
*
* @param featureId string representing the id of the feature for which to record the exposure
* event.
*/
fun NimbusApi.recordExposureEvent(featureId: FeatureId) =
recordExposureEvent(featureId.jsonName)

@ -94,13 +94,11 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.recordExposureEvent
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
@ -119,6 +117,7 @@ import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.topsites.DefaultTopSitesView
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils
@ -390,7 +389,7 @@ class HomeFragment : Fragment() {
activity.themeManager.applyStatusBarTheme(activity)
requireContext().components.analytics.experiments.recordExposureEvent(FeatureId.HOME_PAGE)
FxNimbus.features.homescreen.recordExposure()
if (shouldEnableWallpaper()) {
val wallpaperManger = requireComponents.wallpaperManager

@ -26,10 +26,9 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getVariables
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.whatsnew.WhatsNew
@ -112,7 +111,6 @@ class HomeMenu(
@Suppress("ComplexMethod")
private fun coreMenuItems(): List<BrowserMenuItem> {
val experiments = context.components.analytics.experiments
val settings = context.components.settings
val bookmarksItem = BrowserMenuImageText(
@ -176,10 +174,15 @@ class HomeMenu(
}
// Use nimbus to set the icon and title.
val variables = experiments.getVariables(FeatureId.NIMBUS_VALIDATION)
val nimbusValidation = FxNimbus.features.nimbusValidation.value()
val settingsIcon = context.resources.getIdentifier(
nimbusValidation.settingsIcon,
"drawable",
context.packageName
)
val settingsItem = BrowserMenuImageText(
variables.getText("settings-title") ?: context.getString(R.string.browser_menu_settings),
variables.getDrawableResource("settings-icon") ?: R.drawable.mozac_ic_settings,
nimbusValidation.settingsTitle,
settingsIcon,
primaryTextColor
) {
onItemTapped.invoke(Item.Settings)

@ -40,8 +40,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.AmoCollectionOverrideDialogBinding
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.application
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
@ -50,10 +48,10 @@ import org.mozilla.fenix.ext.navigateToNotificationsSettings
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.REQUEST_CODE_BROWSER_ROLE
import org.mozilla.fenix.ext.getVariables
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.settings.account.AccountUiView
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
@ -161,10 +159,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
super.onResume()
// Use nimbus to set the title, and a trivial addition
val experiments = requireContext().components.analytics.experiments
val variables = experiments.getVariables(FeatureId.NIMBUS_VALIDATION)
val title = variables.getText("settings-title") ?: getString(R.string.settings_title)
val suffix = variables.getString("settings-title-punctuation") ?: ""
val nimbusValidation = FxNimbus.features.nimbusValidation.value()
val title = nimbusValidation.settingsTitle
val suffix = nimbusValidation.settingsPunctuation
showToolbar("$title$suffix")
@ -606,12 +604,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
private fun isDefaultBrowserExperimentBranch(): Boolean {
val experiments = context?.components?.analytics?.experiments
return experiments?.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch ->
(experimentBranch == ExperimentBranch.DEFAULT_BROWSER_SETTINGS_MENU)
} == true
}
private fun isDefaultBrowserExperimentBranch(): Boolean =
FxNimbus.features.defaultBrowserMessage.value().messageLocation == MessageSurfaceId.SETTINGS
private fun isFirefoxDefaultBrowser(): Boolean {
val browsers = BrowsersCache.all(requireContext())

@ -35,11 +35,11 @@ import org.mozilla.fenix.components.settings.counterPreference
import org.mozilla.fenix.components.settings.featureFlagPreference
import org.mozilla.fenix.components.settings.lazyFeatureFlagPreference
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.nimbus.HomeScreenSection
import org.mozilla.fenix.nimbus.MessageSurfaceId
import org.mozilla.fenix.settings.PhoneFeature
import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
@ -120,7 +120,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showTopFrecentSites by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites),
featureFlag = true,
default = { appContext.components.analytics.features.homeScreen.isTopSitesActive() }
default = { homescreenSections[HomeScreenSection.TOP_SITES] == true },
)
var numberOfAppLaunches by intPreference(
@ -327,12 +327,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
*/
fun shouldShowSetAsDefaultBrowserCard(): Boolean {
val browsers = BrowsersCache.all(appContext)
val experiments = appContext.components.analytics.experiments
val isExperimentBranch =
experiments.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch ->
(experimentBranch == ExperimentBranch.DEFAULT_BROWSER_NEW_TAB_BANNER)
}
return isExperimentBranch == true &&
val feature = FxNimbus.features.defaultBrowserMessage.value()
val isExperimentBranch = feature.messageLocation == MessageSurfaceId.HOMESCREEN_BANNER
return isExperimentBranch &&
!userDismissedExperimentCard &&
!browsers.isFirefoxDefaultBrowser &&
numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD
@ -1214,9 +1211,13 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false
)
private val homescreenSections: Map<HomeScreenSection, Boolean> by lazy {
FxNimbus.features.homescreen.value().sectionsEnabled
}
var historyMetadataUIFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_history_metadata_feature),
default = { appContext.components.analytics.features.homeScreen.isRecentExplorationsActive() },
default = { homescreenSections[HomeScreenSection.RECENT_EXPLORATIONS] == true },
featureFlag = FeatureFlags.historyMetadataUIFeature || isHistoryMetadataEnabled
)
@ -1227,7 +1228,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showRecentTabsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_tabs),
featureFlag = FeatureFlags.showRecentTabsFeature,
default = { appContext.components.analytics.features.homeScreen.isRecentlyTabsActive() }
default = { homescreenSections[HomeScreenSection.JUMP_BACK_IN] == true },
)
/**
@ -1236,7 +1237,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
*/
var showRecentBookmarksFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_bookmarks),
default = { appContext.components.analytics.features.homeScreen.isRecentlySavedActive() },
default = { homescreenSections[HomeScreenSection.RECENTLY_SAVED] == true },
featureFlag = FeatureFlags.recentBookmarksFeature
)
@ -1268,7 +1269,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showPocketRecommendationsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_pocket_homescreen_recommendations),
featureFlag = FeatureFlags.isPocketRecommendationsFeatureEnabled(appContext),
default = { appContext.components.analytics.features.homeScreen.isPocketRecommendationsActive() },
default = { homescreenSections[HomeScreenSection.POCKET] == true },
)
/**

@ -10,6 +10,7 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
import mozilla.components.concept.fetch.Client
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse
@ -25,6 +26,7 @@ import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings
import org.robolectric.Robolectric
import java.io.IOException
@ -49,6 +51,8 @@ class SettingsFragmentTest {
mockkObject(Config)
every { Config.channel } returns ReleaseChannel.Nightly
FxNimbus.api = NimbusDisabled(testContext)
}
@Test

@ -70,6 +70,7 @@ buildscript {
classpath Deps.osslicenses_plugin
classpath "org.mozilla.components:tooling-glean-gradle:${Versions.mozilla_android_components}"
classpath "org.mozilla.components:tooling-nimbus-gradle:${Versions.mozilla_android_components}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

@ -0,0 +1,89 @@
---
channels:
- release
- beta
- nightly
- developer
features:
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled.
If the section is enabled, it should be toggleable in the settings screen, and on by default."
type: Map<HomeScreenSection, Boolean>
default:
{
"top-sites": true,
"jump-back-in": true,
"recently-saved": true,
"recent-explorations": true,
"pocket": true
}
defaults:
- channel: nightly
value: {
"sections-enabled": {
"top-sites": true,
"jump-back-in": true,
"recently-saved": true,
"recent-explorations": true,
"pocket": true
}
}
nimbus-validation:
description: "A feature that does not correspond to an application feature suitable for showing
that Nimbus is working. This should never be used in production."
variables:
settings-title:
description: The title of displayed in the Settings screen and app menu.
type: Text
default: browser_menu_settings
settings-punctuation:
description: The emoji displayed in the Settings screen title.
type: String
default: ""
settings-icon:
description: The drawable displayed in the app menu for Settings
type: String
default: mozac_ic_settings
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
variables:
enabled:
description: If true, the feature shows up on the homescreen and on the new tab screen.
type: Boolean
default: false
default-browser-message:
description: A small feature allowing experiments on the placement of a default browser message.
variables:
message-location:
description: Where is the message to be put.
type: MessageSurfaceId
default: homescreen-banner
types:
objects: {}
enums:
HomeScreenSection:
description: The identifiers for the sections of the homescreen.
variants:
top-sites:
description: The frecency and pinned sites.
recently-saved:
description: The sites the user has bookmarked recently.
jump-back-in:
description: The tabs the user was looking immediately before being interrupted.
recent-explorations:
description: The tab groups
pocket:
description: The pocket section. This should only be available in the US.
MessageSurfaceId:
description: The identity of a message surface, used in the default browser experiments
variants:
app-menu-item:
description: An item in the default toolbar menu.
settings:
description: A setting in the settings screen.
homescreen-banner:
description: A banner in the homescreen.
Loading…
Cancel
Save