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. // Generate Kotlin code for the Fenix Glean metrics.
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin" 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 { configurations {
// There's an interaction between Gradle's resolution of dependencies with different types // 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 // (@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.Event
import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.MozillaProductDetector 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.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.isCustomEngine import org.mozilla.fenix.ext.isCustomEngine
import org.mozilla.fenix.ext.isKnownSearchDomain import org.mozilla.fenix.ext.isKnownSearchDomain
@ -273,8 +272,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS 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. // Account manager initialization needs to happen on the main thread.
GlobalScope.launch(Dispatchers.Main) { 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.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.NimbusFeatures
import org.mozilla.fenix.experiments.createNimbus import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
@ -109,11 +109,9 @@ class Analytics(
} }
val experiments: NimbusApi by lazyMonitored { val experiments: NimbusApi by lazyMonitored {
createNimbus(context, BuildConfig.NIMBUS_ENDPOINT) createNimbus(context, BuildConfig.NIMBUS_ENDPOINT).also { api ->
} FxNimbus.api = api
}
val features: NimbusFeatures by lazyMonitored {
NimbusFeatures(context)
} }
} }

@ -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 mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager 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.components
import org.mozilla.fenix.ext.settings 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.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
@ -361,6 +360,7 @@ open class DefaultToolbarMenu(
@VisibleForTesting(otherwise = PRIVATE) @VisibleForTesting(otherwise = PRIVATE)
val coreMenuItems by lazy { val coreMenuItems by lazy {
val defaultBrowserItem = getSetDefaultBrowserItem()
val menuItems = val menuItems =
listOfNotNull( listOfNotNull(
if (shouldUseBottomToolbar) null else menuToolbar, if (shouldUseBottomToolbar) null else menuToolbar,
@ -372,8 +372,8 @@ open class DefaultToolbarMenu(
extensionsItem, extensionsItem,
syncMenuItem, syncMenuItem,
BrowserMenuDivider(), BrowserMenuDivider(),
getSetDefaultBrowserItem(), defaultBrowserItem,
getSetDefaultBrowserItem()?.let { BrowserMenuDivider() }, defaultBrowserItem?.let { BrowserMenuDivider() },
findInPageItem, findInPageItem,
desktopSiteItem, desktopSiteItem,
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },
@ -446,23 +446,17 @@ open class DefaultToolbarMenu(
} }
} }
private fun getSetDefaultBrowserItem(): BrowserMenuImageText? { private fun getSetDefaultBrowserItem(): BrowserMenuImageText? =
val experiments = context.components.analytics.experiments if (BrowsersCache.all(context).isFirefoxDefaultBrowser) {
val browsers = BrowsersCache.all(context) null
} else if (FxNimbus.features.defaultBrowserMessage.value().messageLocation == MessageSurfaceId.APP_MENU_ITEM) {
return experiments.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch -> BrowserMenuImageText(
if (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_TOOLBAR_MENU && label = context.getString(R.string.preferences_set_as_default_browser),
!browsers.isFirefoxDefaultBrowser imageResource = R.mipmap.ic_launcher
) { ) {
return@withExperiment BrowserMenuImageText( onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
label = context.getString(R.string.preferences_set_as_default_browser),
imageResource = R.mipmap.ic_launcher
) {
onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
}
} else {
null
} }
} 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.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentHomeBinding import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.asRecentTabs import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.recordExposureEvent
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings 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.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.topsites.DefaultTopSitesView import org.mozilla.fenix.home.topsites.DefaultTopSitesView
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
@ -390,7 +389,7 @@ class HomeFragment : Fragment() {
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
requireContext().components.analytics.experiments.recordExposureEvent(FeatureId.HOME_PAGE) FxNimbus.features.homescreen.recordExposure()
if (shouldEnableWallpaper()) { if (shouldEnableWallpaper()) {
val wallpaperManger = requireComponents.wallpaperManager 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.R
import org.mozilla.fenix.components.accounts.AccountState import org.mozilla.fenix.components.accounts.AccountState
import org.mozilla.fenix.components.accounts.FenixAccountManager import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getVariables
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.whatsnew.WhatsNew import org.mozilla.fenix.whatsnew.WhatsNew
@ -112,7 +111,6 @@ class HomeMenu(
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
private fun coreMenuItems(): List<BrowserMenuItem> { private fun coreMenuItems(): List<BrowserMenuItem> {
val experiments = context.components.analytics.experiments
val settings = context.components.settings val settings = context.components.settings
val bookmarksItem = BrowserMenuImageText( val bookmarksItem = BrowserMenuImageText(
@ -176,10 +174,15 @@ class HomeMenu(
} }
// Use nimbus to set the icon and title. // 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( val settingsItem = BrowserMenuImageText(
variables.getText("settings-title") ?: context.getString(R.string.browser_menu_settings), nimbusValidation.settingsTitle,
variables.getDrawableResource("settings-icon") ?: R.drawable.mozac_ic_settings, settingsIcon,
primaryTextColor primaryTextColor
) { ) {
onItemTapped.invoke(Item.Settings) onItemTapped.invoke(Item.Settings)

@ -40,8 +40,6 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.AmoCollectionOverrideDialogBinding 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.application
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey 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.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.REQUEST_CODE_BROWSER_ROLE 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.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.showToolbar 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.settings.account.AccountUiView
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -161,10 +159,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
super.onResume() super.onResume()
// Use nimbus to set the title, and a trivial addition // Use nimbus to set the title, and a trivial addition
val experiments = requireContext().components.analytics.experiments val nimbusValidation = FxNimbus.features.nimbusValidation.value()
val variables = experiments.getVariables(FeatureId.NIMBUS_VALIDATION)
val title = variables.getText("settings-title") ?: getString(R.string.settings_title) val title = nimbusValidation.settingsTitle
val suffix = variables.getString("settings-title-punctuation") ?: "" val suffix = nimbusValidation.settingsPunctuation
showToolbar("$title$suffix") showToolbar("$title$suffix")
@ -606,12 +604,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
} }
private fun isDefaultBrowserExperimentBranch(): Boolean { private fun isDefaultBrowserExperimentBranch(): Boolean =
val experiments = context?.components?.analytics?.experiments FxNimbus.features.defaultBrowserMessage.value().messageLocation == MessageSurfaceId.SETTINGS
return experiments?.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch ->
(experimentBranch == ExperimentBranch.DEFAULT_BROWSER_SETTINGS_MENU)
} == true
}
private fun isFirefoxDefaultBrowser(): Boolean { private fun isFirefoxDefaultBrowser(): Boolean {
val browsers = BrowsersCache.all(requireContext()) 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.featureFlagPreference
import org.mozilla.fenix.components.settings.lazyFeatureFlagPreference import org.mozilla.fenix.components.settings.lazyFeatureFlagPreference
import org.mozilla.fenix.components.toolbar.ToolbarPosition 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.components
import org.mozilla.fenix.ext.getPreferenceKey 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.PhoneFeature
import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType
import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu
@ -120,7 +120,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showTopFrecentSites by lazyFeatureFlagPreference( var showTopFrecentSites by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites), appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites),
featureFlag = true, featureFlag = true,
default = { appContext.components.analytics.features.homeScreen.isTopSitesActive() } default = { homescreenSections[HomeScreenSection.TOP_SITES] == true },
) )
var numberOfAppLaunches by intPreference( var numberOfAppLaunches by intPreference(
@ -327,12 +327,9 @@ class Settings(private val appContext: Context) : PreferencesHolder {
*/ */
fun shouldShowSetAsDefaultBrowserCard(): Boolean { fun shouldShowSetAsDefaultBrowserCard(): Boolean {
val browsers = BrowsersCache.all(appContext) val browsers = BrowsersCache.all(appContext)
val experiments = appContext.components.analytics.experiments val feature = FxNimbus.features.defaultBrowserMessage.value()
val isExperimentBranch = val isExperimentBranch = feature.messageLocation == MessageSurfaceId.HOMESCREEN_BANNER
experiments.withExperiment(FeatureId.DEFAULT_BROWSER) { experimentBranch -> return isExperimentBranch &&
(experimentBranch == ExperimentBranch.DEFAULT_BROWSER_NEW_TAB_BANNER)
}
return isExperimentBranch == true &&
!userDismissedExperimentCard && !userDismissedExperimentCard &&
!browsers.isFirefoxDefaultBrowser && !browsers.isFirefoxDefaultBrowser &&
numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD
@ -1214,9 +1211,13 @@ class Settings(private val appContext: Context) : PreferencesHolder {
default = false default = false
) )
private val homescreenSections: Map<HomeScreenSection, Boolean> by lazy {
FxNimbus.features.homescreen.value().sectionsEnabled
}
var historyMetadataUIFeature by lazyFeatureFlagPreference( var historyMetadataUIFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_history_metadata_feature), 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 featureFlag = FeatureFlags.historyMetadataUIFeature || isHistoryMetadataEnabled
) )
@ -1227,7 +1228,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showRecentTabsFeature by lazyFeatureFlagPreference( var showRecentTabsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_tabs), appContext.getPreferenceKey(R.string.pref_key_recent_tabs),
featureFlag = FeatureFlags.showRecentTabsFeature, 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( var showRecentBookmarksFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_recent_bookmarks), appContext.getPreferenceKey(R.string.pref_key_recent_bookmarks),
default = { appContext.components.analytics.features.homeScreen.isRecentlySavedActive() }, default = { homescreenSections[HomeScreenSection.RECENTLY_SAVED] == true },
featureFlag = FeatureFlags.recentBookmarksFeature featureFlag = FeatureFlags.recentBookmarksFeature
) )
@ -1268,7 +1269,7 @@ class Settings(private val appContext: Context) : PreferencesHolder {
var showPocketRecommendationsFeature by lazyFeatureFlagPreference( var showPocketRecommendationsFeature by lazyFeatureFlagPreference(
appContext.getPreferenceKey(R.string.pref_key_pocket_homescreen_recommendations), appContext.getPreferenceKey(R.string.pref_key_pocket_homescreen_recommendations),
featureFlag = FeatureFlags.isPocketRecommendationsFeatureEnabled(appContext), 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.mockk
import io.mockk.mockkObject import io.mockk.mockkObject
import mozilla.components.concept.fetch.Client import mozilla.components.concept.fetch.Client
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.robolectric.testContext
import mozilla.components.support.test.rule.MainCoroutineRule import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.Assert.assertFalse 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.components
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import org.robolectric.Robolectric import org.robolectric.Robolectric
import java.io.IOException import java.io.IOException
@ -49,6 +51,8 @@ class SettingsFragmentTest {
mockkObject(Config) mockkObject(Config)
every { Config.channel } returns ReleaseChannel.Nightly every { Config.channel } returns ReleaseChannel.Nightly
FxNimbus.api = NimbusDisabled(testContext)
} }
@Test @Test

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