Merge tag 'v90.1.2' into upstream-sync
commit
2380d51808
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.assertion.ViewAssertions.matches
|
||||
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.uiautomator.UiSelector
|
||||
import junit.framework.TestCase.assertTrue
|
||||
import mozilla.components.support.ktx.android.content.appName
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
|
||||
|
||||
/**
|
||||
* Implementation of the robot pattern for Custom tabs
|
||||
*/
|
||||
class CustomTabRobot {
|
||||
|
||||
fun verifyDesktopSiteButtonExists() {
|
||||
desktopSiteButton().check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun verifyFindInPageButtonExists() {
|
||||
findInPageButton().check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun verifyPoweredByTextIsDisplayed() {
|
||||
mDevice.findObject(UiSelector().textContains("POWERED BY ${appContext.appName}"))
|
||||
}
|
||||
|
||||
fun verifyOpenInBrowserButtonExists() {
|
||||
openInBrowserButton().check(matches(isDisplayed()))
|
||||
}
|
||||
|
||||
fun verifyBackButtonExists() = assertTrue(backButton().waitForExists(waitingTime))
|
||||
|
||||
fun verifyForwardButtonExists() = assertTrue(forwardButton().waitForExists(waitingTime))
|
||||
|
||||
fun verifyRefreshButtonExists() = assertTrue(refreshButton().waitForExists(waitingTime))
|
||||
|
||||
fun verifyCustomMenuItem(label: String) {
|
||||
assertTrue(mDevice.findObject(UiSelector().text(label)).exists())
|
||||
}
|
||||
|
||||
class Transition {
|
||||
fun openMainMenu(interact: CustomTabRobot.() -> Unit): Transition {
|
||||
mainMenuButton().waitForExists(waitingTime)
|
||||
mainMenuButton().click()
|
||||
|
||||
CustomTabRobot().interact()
|
||||
return Transition()
|
||||
}
|
||||
|
||||
fun clickOpenInBrowserButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
|
||||
openInBrowserButton().perform(click())
|
||||
|
||||
BrowserRobot().interact()
|
||||
return BrowserRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun customTabScreen(interact: CustomTabRobot.() -> Unit): CustomTabRobot.Transition {
|
||||
CustomTabRobot().interact()
|
||||
return CustomTabRobot.Transition()
|
||||
}
|
||||
|
||||
private fun mainMenuButton() = mDevice.findObject(UiSelector().description("Menu"))
|
||||
|
||||
private fun desktopSiteButton() = onView(withId(R.id.switch_widget))
|
||||
|
||||
private fun findInPageButton() = onView(withText("Find in page"))
|
||||
|
||||
private fun openInBrowserButton() = onView(withText("Open in ${appContext.appName}"))
|
||||
|
||||
private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
|
||||
|
||||
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
|
||||
|
||||
private fun backButton() = mDevice.findObject(UiSelector().description("Back"))
|
@ -0,0 +1,29 @@
|
||||
/* 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.ui.robots
|
||||
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
import org.hamcrest.CoreMatchers
|
||||
|
||||
class SettingsSubMenuLanguageRobot {
|
||||
class Transition {
|
||||
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
mDevice.waitForIdle()
|
||||
goBackButton().perform(ViewActions.click())
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackButton() =
|
||||
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
|
@ -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.ui.robots
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.uiautomator.UiDevice
|
||||
|
||||
class SettingsSubMenuSetDefaultBrowserRobot {
|
||||
class Transition {
|
||||
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
|
||||
|
||||
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
|
||||
mDevice.waitForIdle()
|
||||
|
||||
// We are now in system settings / showing a default browser dialog.
|
||||
// Really want to go back to the app. Not interested in up navigation like in other robots.
|
||||
mDevice.pressBack()
|
||||
|
||||
SettingsRobot().interact()
|
||||
return SettingsRobot.Transition()
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
{
|
||||
"data": [{
|
||||
"slug": "feature-text-variables-validation-android",
|
||||
"appId": "org.mozilla.fenix",
|
||||
"appName": "fenix",
|
||||
"channel": "nightly",
|
||||
"branches": [{
|
||||
"slug": "control",
|
||||
"ratio": 100,
|
||||
"feature": {
|
||||
"value": {},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "a1",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "settings_title",
|
||||
"settings-title-punctuation": "…"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "a2",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "preferences_category_general",
|
||||
"settings-title-punctuation": "!"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"arguments": {},
|
||||
"probeSets": [],
|
||||
"startDate": null,
|
||||
"targeting": "true",
|
||||
"featureIds": [
|
||||
"nimbus-validation"
|
||||
],
|
||||
"application": "org.mozilla.firefox_beta",
|
||||
"bucketConfig": {
|
||||
"count": 0,
|
||||
"start": 0,
|
||||
"total": 10000,
|
||||
"namespace": "nimbus-validation-2",
|
||||
"randomizationUnit": "nimbus_id"
|
||||
},
|
||||
"schemaVersion": "1.5.0",
|
||||
"userFacingName": "Nimbus Text Variables Validation",
|
||||
"referenceBranch": "control",
|
||||
"proposedDuration": 14,
|
||||
"isEnrollmentPaused": false,
|
||||
"proposedEnrollment": 7,
|
||||
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
|
||||
"last_modified": 1621443780172
|
||||
},
|
||||
{
|
||||
"slug": "feature-icon-variables-validation-android",
|
||||
"appId": "org.mozilla.fenix",
|
||||
"appName": "fenix",
|
||||
"channel": "nightly",
|
||||
"branches": [{
|
||||
"slug": "control",
|
||||
"ratio": 100,
|
||||
"feature": {
|
||||
"value": {},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "treatment",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "Fancy Settings",
|
||||
"settings-icon": "ic_edit"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"arguments": {},
|
||||
"probeSets": [],
|
||||
"startDate": null,
|
||||
"targeting": "true",
|
||||
"featureIds": [
|
||||
"nimbus-validation"
|
||||
],
|
||||
"application": "org.mozilla.firefox_beta",
|
||||
"bucketConfig": {
|
||||
"count": 0,
|
||||
"start": 0,
|
||||
"total": 10000,
|
||||
"namespace": "nimbus-validation-2",
|
||||
"randomizationUnit": "nimbus_id"
|
||||
},
|
||||
"schemaVersion": "1.5.0",
|
||||
"userFacingName": "Nimbus Icon Variables Validation",
|
||||
"referenceBranch": "control",
|
||||
"proposedDuration": 14,
|
||||
"isEnrollmentPaused": false,
|
||||
"proposedEnrollment": 7,
|
||||
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
|
||||
"last_modified": 1621443780172
|
||||
},
|
||||
|
||||
{
|
||||
"slug": "feature-text-variables-validation-ios",
|
||||
"appId": "org.mozilla.ios.Fennec",
|
||||
"appName": "firefox_ios",
|
||||
"channel": "nightly",
|
||||
"branches": [{
|
||||
"slug": "control",
|
||||
"ratio": 100,
|
||||
"feature": {
|
||||
"value": {},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "a1",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "Menu/Menu.OpenSettingsAction.Title",
|
||||
"settings-title-punctuation": "…"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "a2",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "Settings.General.SectionName",
|
||||
"settings-title-punctuation": "!"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"arguments": {},
|
||||
"probeSets": [],
|
||||
"startDate": null,
|
||||
"targeting": "true",
|
||||
"featureIds": [
|
||||
"nimbus-validation"
|
||||
],
|
||||
"application": "org.mozilla.ios.Fennec",
|
||||
"bucketConfig": {
|
||||
"count": 0,
|
||||
"start": 0,
|
||||
"total": 10000,
|
||||
"namespace": "nimbus-validation-2",
|
||||
"randomizationUnit": "nimbus_id"
|
||||
},
|
||||
"schemaVersion": "1.5.0",
|
||||
"userFacingName": "Nimbus Text Variables Validation",
|
||||
"referenceBranch": "control",
|
||||
"proposedDuration": 14,
|
||||
"isEnrollmentPaused": false,
|
||||
"proposedEnrollment": 7,
|
||||
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
|
||||
"last_modified": 1621443780172
|
||||
},
|
||||
{
|
||||
"slug": "feature-icon-variables-validation-ios",
|
||||
"appId": "org.mozilla.ios.Fennec",
|
||||
"appName": "firefox_ios",
|
||||
"channel": "nightly",
|
||||
"branches": [{
|
||||
"slug": "control",
|
||||
"ratio": 100,
|
||||
"feature": {
|
||||
"value": {},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
},
|
||||
{
|
||||
"slug": "treatment",
|
||||
"ratio": 0,
|
||||
"feature": {
|
||||
"value": {
|
||||
"settings-title": "Fancy Settings",
|
||||
"settings-icon": "menu-ViewMobile"
|
||||
},
|
||||
"enabled": true,
|
||||
"featureId": "nimbus-validation"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outcomes": [],
|
||||
"arguments": {},
|
||||
"probeSets": [],
|
||||
"startDate": null,
|
||||
"targeting": "true",
|
||||
"featureIds": [
|
||||
"nimbus-validation"
|
||||
],
|
||||
"application": "org.mozilla.ios.Fennec",
|
||||
"bucketConfig": {
|
||||
"count": 0,
|
||||
"start": 0,
|
||||
"total": 10000,
|
||||
"namespace": "nimbus-validation-2",
|
||||
"randomizationUnit": "nimbus_id"
|
||||
},
|
||||
"schemaVersion": "1.5.0",
|
||||
"userFacingName": "Nimbus Icon Variables Validation",
|
||||
"referenceBranch": "control",
|
||||
"proposedDuration": 14,
|
||||
"isEnrollmentPaused": false,
|
||||
"proposedEnrollment": 7,
|
||||
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
|
||||
"last_modified": 1621443780172
|
||||
}
|
||||
]
|
||||
}
|
@ -1,91 +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/. */
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.engine.gecko.autofill.GeckoLoginDelegateWrapper
|
||||
import mozilla.components.browser.engine.gecko.ext.toContentBlockingSetting
|
||||
import mozilla.components.browser.engine.gecko.glean.GeckoAdapter
|
||||
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
|
||||
import mozilla.components.concept.storage.LoginsStorage
|
||||
import mozilla.components.lib.crash.handler.CrashHandlerService
|
||||
import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate
|
||||
import org.mozilla.fenix.Config
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.geckoview.GeckoRuntime
|
||||
import org.mozilla.geckoview.GeckoRuntimeSettings
|
||||
import org.mozilla.geckoview.ContentBlocking
|
||||
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
|
||||
|
||||
object GeckoProvider {
|
||||
private var runtime: GeckoRuntime? = null
|
||||
const val CN_UPDATE_URL =
|
||||
"https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
|
||||
const val CN_GET_HASH_URL =
|
||||
"https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreateRuntime(
|
||||
context: Context,
|
||||
storage: Lazy<LoginsStorage>,
|
||||
trackingProtectionPolicy: TrackingProtectionPolicy
|
||||
): GeckoRuntime {
|
||||
if (runtime == null) {
|
||||
runtime = createRuntime(context, storage, trackingProtectionPolicy)
|
||||
}
|
||||
|
||||
return runtime!!
|
||||
}
|
||||
|
||||
private fun createRuntime(
|
||||
context: Context,
|
||||
storage: Lazy<LoginsStorage>,
|
||||
policy: TrackingProtectionPolicy
|
||||
): GeckoRuntime {
|
||||
val builder = GeckoRuntimeSettings.Builder()
|
||||
|
||||
val runtimeSettings = builder
|
||||
.crashHandler(CrashHandlerService::class.java)
|
||||
.telemetryDelegate(GeckoAdapter())
|
||||
.contentBlocking(policy.toContentBlockingSetting())
|
||||
.debugLogging(Config.channel.isDebug)
|
||||
.aboutConfigEnabled(true)
|
||||
.build()
|
||||
|
||||
val settings = context.components.settings
|
||||
if (!settings.shouldUseAutoSize) {
|
||||
runtimeSettings.automaticFontSizeAdjustment = false
|
||||
val fontSize = settings.fontSizeFactor
|
||||
runtimeSettings.fontSizeFactor = fontSize
|
||||
}
|
||||
|
||||
// Add safebrowsing providers for China
|
||||
if (Config.channel.isMozillaOnline) {
|
||||
val mozcn = SafeBrowsingProvider
|
||||
.withName("mozcn")
|
||||
.version("2.2")
|
||||
.lists("m6eb-phish-shavar", "m6ib-phish-shavar")
|
||||
.updateUrl(CN_UPDATE_URL)
|
||||
.getHashUrl(CN_GET_HASH_URL)
|
||||
.build()
|
||||
|
||||
runtimeSettings.contentBlocking.setSafeBrowsingProviders(mozcn,
|
||||
// Keep the existing configuration
|
||||
ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER,
|
||||
ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
|
||||
|
||||
runtimeSettings.contentBlocking.setSafeBrowsingPhishingTable(
|
||||
"m6eb-phish-shavar",
|
||||
"m6ib-phish-shavar",
|
||||
// Existing configuration
|
||||
"goog-phish-proto")
|
||||
}
|
||||
|
||||
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
|
||||
val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
|
||||
@Suppress("Deprecation")
|
||||
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
|
||||
|
||||
return geckoRuntime
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
/* 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
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.mozilla.fenix.ext.removeSecure
|
||||
import org.mozilla.fenix.ext.secure
|
||||
|
||||
/**
|
||||
* A [Fragment] implementation that can be used to secure screens displaying sensitive information
|
||||
* by not allowing taking screenshots of their content.
|
||||
*
|
||||
* Fragments displaying such screens should extend [SecureFragment] instead of [Fragment] class.
|
||||
*/
|
||||
open class SecureFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) {
|
||||
|
||||
constructor() : this(0) {
|
||||
Fragment()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
this.secure()
|
||||
super.onCreate(savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
this.removeSecure()
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/* 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.nimbus
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.launch
|
||||
import mozilla.components.lib.state.ext.consumeFrom
|
||||
import mozilla.components.support.base.log.logger.Logger
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.StoreProvider
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
import org.mozilla.fenix.nimbus.controller.NimbusBranchesController
|
||||
import org.mozilla.fenix.nimbus.view.NimbusBranchesView
|
||||
|
||||
/**
|
||||
* A fragment to show the branches of a Nimbus experiment.
|
||||
*/
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
class NimbusBranchesFragment : Fragment() {
|
||||
|
||||
private lateinit var nimbusBranchesStore: NimbusBranchesStore
|
||||
private lateinit var nimbusBranchesView: NimbusBranchesView
|
||||
private lateinit var controller: NimbusBranchesController
|
||||
|
||||
private val args by navArgs<NimbusBranchesFragmentArgs>()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view =
|
||||
inflater.inflate(R.layout.mozac_service_nimbus_experiment_details, container, false)
|
||||
|
||||
nimbusBranchesStore = StoreProvider.get(this) {
|
||||
NimbusBranchesStore(NimbusBranchesState(branches = emptyList()))
|
||||
}
|
||||
|
||||
controller = NimbusBranchesController(
|
||||
nimbusBranchesStore = nimbusBranchesStore,
|
||||
experiments = requireContext().components.analytics.experiments,
|
||||
experimentId = args.experimentId
|
||||
)
|
||||
|
||||
nimbusBranchesView =
|
||||
NimbusBranchesView(view.findViewById(R.id.nimbus_experiment_branches_list), controller)
|
||||
|
||||
loadExperimentBranches()
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
consumeFrom(nimbusBranchesStore) { state ->
|
||||
nimbusBranchesView.update(state)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showToolbar(args.experimentName)
|
||||
}
|
||||
|
||||
private fun loadExperimentBranches() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val experiments = requireContext().components.analytics.experiments
|
||||
val branches = experiments.getExperimentBranches(args.experimentId) ?: emptyList()
|
||||
val selectedBranch = experiments.getExperimentBranch(args.experimentId) ?: ""
|
||||
|
||||
nimbusBranchesStore.dispatch(
|
||||
NimbusBranchesAction.UpdateBranches(
|
||||
branches,
|
||||
selectedBranch
|
||||
)
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
Logger.error("Failed to getActiveExperiments()", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
/* 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.nimbus
|
||||
|
||||
import mozilla.components.lib.state.Action
|
||||
import mozilla.components.lib.state.State
|
||||
import mozilla.components.lib.state.Store
|
||||
import org.mozilla.experiments.nimbus.Branch
|
||||
|
||||
/**
|
||||
* The [Store] for holding the [NimbusBranchesState] and applying [NimbusBranchesAction]s.
|
||||
*/
|
||||
class NimbusBranchesStore(initialState: NimbusBranchesState) :
|
||||
Store<NimbusBranchesState, NimbusBranchesAction>(
|
||||
initialState, ::nimbusBranchesFragmentStateReducer
|
||||
)
|
||||
|
||||
/**
|
||||
* The state for [NimbusBranchesFragment].
|
||||
*
|
||||
* @property branches The list of [Branch]s to display in the branches list.
|
||||
* @property selectedBranch The selected [Branch] slug for a Nimbus experiment.
|
||||
* @property isLoading True if the branches are still being loaded from storage, otherwise false.
|
||||
*/
|
||||
data class NimbusBranchesState(
|
||||
val branches: List<Branch>,
|
||||
val selectedBranch: String = "",
|
||||
val isLoading: Boolean = true
|
||||
) : State
|
||||
|
||||
/**
|
||||
* Actions to dispatch through the [NimbusBranchesStore] to modify the [NimbusBranchesState]
|
||||
* through the [nimbusBranchesFragmentStateReducer].
|
||||
*/
|
||||
sealed class NimbusBranchesAction : Action {
|
||||
/**
|
||||
* Updates the list of Nimbus branches and selected branch.
|
||||
*
|
||||
* @param branches The list of [Branch]s to display in the branches list.
|
||||
* @param selectedBranch The selected [Branch] slug for a Nimbus experiment.
|
||||
*/
|
||||
data class UpdateBranches(val branches: List<Branch>, val selectedBranch: String) :
|
||||
NimbusBranchesAction()
|
||||
|
||||
/**
|
||||
* Updates the selected branch.
|
||||
*
|
||||
* @param selectedBranch The selected [Branch] slug for a Nimbus experiment.
|
||||
*/
|
||||
data class UpdateSelectedBranch(val selectedBranch: String) : NimbusBranchesAction()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the Nimbus branches state from the current state with the provided [action] to
|
||||
* be performed.
|
||||
*
|
||||
* @param state The current Nimbus branches state.
|
||||
* @param action The action to be performed on the state.
|
||||
* @return the new [NimbusBranchesState] with the [action] executed.
|
||||
*/
|
||||
private fun nimbusBranchesFragmentStateReducer(
|
||||
state: NimbusBranchesState,
|
||||
action: NimbusBranchesAction
|
||||
): NimbusBranchesState {
|
||||
return when (action) {
|
||||
is NimbusBranchesAction.UpdateBranches -> {
|
||||
state.copy(
|
||||
branches = action.branches,
|
||||
selectedBranch = action.selectedBranch,
|
||||
isLoading = false
|
||||
)
|
||||
}
|
||||
is NimbusBranchesAction.UpdateSelectedBranch -> {
|
||||
state.copy(selectedBranch = action.selectedBranch)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +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.nimbus
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import mozilla.components.service.nimbus.ui.NimbusDetailAdapter
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.ext.showToolbar
|
||||
|
||||
/**
|
||||
* A fragment to show the details of a Nimbus experiment.
|
||||
*/
|
||||
class NimbusDetailsFragment : Fragment(R.layout.mozac_service_nimbus_experiment_details) {
|
||||
|
||||
private val args by navArgs<NimbusDetailsFragmentArgs>()
|
||||
private var adapter: NimbusDetailAdapter? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
bindRecyclerView(view)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
showToolbar(args.experiment)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
// Letting go of the resources to avoid memory leak.
|
||||
adapter = null
|
||||
}
|
||||
|
||||
private fun bindRecyclerView(view: View) {
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.nimbus_experiment_branches_list)
|
||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||
|
||||
val shouldRefresh = adapter != null
|
||||
|
||||
// Dummy data until we have the appropriate Nimbus API.
|
||||
val branches = listOf(
|
||||
"Control",
|
||||
"Treatment"
|
||||
)
|
||||
|
||||
if (!shouldRefresh) {
|
||||
adapter = NimbusDetailAdapter(branches)
|
||||
}
|
||||
|
||||
recyclerView.adapter = adapter
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
/* 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.nimbus.controller
|
||||
|
||||
import mozilla.components.service.nimbus.NimbusApi
|
||||
import mozilla.components.service.nimbus.ui.NimbusBranchesAdapterDelegate
|
||||
import org.mozilla.experiments.nimbus.Branch
|
||||
import org.mozilla.fenix.nimbus.NimbusBranchesAction
|
||||
import org.mozilla.fenix.nimbus.NimbusBranchesStore
|
||||
|
||||
/**
|
||||
* [NimbusBranchesFragment] controller. This implements [NimbusBranchesAdapterDelegate] to handle
|
||||
* interactions with a Nimbus branch item.
|
||||
*
|
||||
* @param nimbusBranchesStore An instance of [NimbusBranchesStore] for dispatching
|
||||
* [NimbusBranchesAction]s.
|
||||
* @param experiments An instance of [NimbusApi] for interacting with the Nimbus experiments.
|
||||
* @param experimentId The string experiment-id or "slug" for a Nimbus experiment.
|
||||
*/
|
||||
class NimbusBranchesController(
|
||||
private val nimbusBranchesStore: NimbusBranchesStore,
|
||||
private val experiments: NimbusApi,
|
||||
private val experimentId: String
|
||||
) : NimbusBranchesAdapterDelegate {
|
||||
|
||||
override fun onBranchItemClicked(branch: Branch) {
|
||||
experiments.optInWithBranch(experimentId, branch.slug)
|
||||
nimbusBranchesStore.dispatch(NimbusBranchesAction.UpdateSelectedBranch(branch.slug))
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
/* 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.nimbus.view
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import mozilla.components.service.nimbus.ui.NimbusBranchAdapter
|
||||
import org.mozilla.fenix.nimbus.NimbusBranchesState
|
||||
import org.mozilla.fenix.nimbus.controller.NimbusBranchesController
|
||||
|
||||
/**
|
||||
* View used for managing a Nimbus experiment's branches.
|
||||
*/
|
||||
class NimbusBranchesView(
|
||||
override val containerView: ViewGroup,
|
||||
val controller: NimbusBranchesController
|
||||
) : LayoutContainer {
|
||||
|
||||
private val nimbusAdapter = NimbusBranchAdapter(controller)
|
||||
|
||||
init {
|
||||
val recyclerView: RecyclerView = containerView as RecyclerView
|
||||
recyclerView.apply {
|
||||
adapter = nimbusAdapter
|
||||
layoutManager = LinearLayoutManager(containerView.context)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(state: NimbusBranchesState) {
|
||||
nimbusAdapter.updateData(state.branches, state.selectedBranch)
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/* 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.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SwitchCompat
|
||||
import androidx.preference.PreferenceViewHolder
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import org.mozilla.fenix.R
|
||||
|
||||
/**
|
||||
* Variation of [SwitchPreferenceCompat] that uses a custom widgetLayoutResource in order to implement
|
||||
* visibility changes to it.
|
||||
* */
|
||||
class SyncPreference @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : SwitchPreferenceCompat(context, attrs) {
|
||||
|
||||
private var switchView: SwitchCompat? = null
|
||||
|
||||
/**
|
||||
* Whether or not switch's toggle widget is visible.
|
||||
* */
|
||||
var isSwitchWidgetVisible: Boolean = false
|
||||
|
||||
init {
|
||||
widgetLayoutResource = R.layout.preference_sync
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the switch state.
|
||||
* */
|
||||
internal fun setSwitchCheckedState(isChecked: Boolean) {
|
||||
switchView?.isChecked = isChecked
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
super.onBindViewHolder(holder)
|
||||
|
||||
switchView = holder.findViewById(R.id.switch_widget) as SwitchCompat?
|
||||
|
||||
switchView?.isChecked = isChecked
|
||||
switchView?.visibility = if (isSwitchWidgetVisible) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/* 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.settings.biometric
|
||||
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.View
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
|
||||
import org.mozilla.fenix.ext.settings
|
||||
import org.mozilla.fenix.settings.requirePreference
|
||||
|
||||
/**
|
||||
* Helper for creating and implementing the [BiometricPromptFeature]. Currently used
|
||||
* for logins and credit cards.
|
||||
*/
|
||||
abstract class BiometricPromptPreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val biometricPromptFeature = ViewBoundFeatureWrapper<BiometricPromptFeature>()
|
||||
|
||||
/**
|
||||
* Gets the string to be used for [BiometricPromptFeature.requestAuthentication] prompting to
|
||||
* unlock the device.
|
||||
*/
|
||||
abstract fun unlockMessage(): String
|
||||
|
||||
/**
|
||||
* Navigate when authentication is successful.
|
||||
*/
|
||||
abstract fun navigateOnSuccess()
|
||||
|
||||
/**
|
||||
* Shows a dialog warning to set up a pin/password when the device is not secured. This is
|
||||
* only used when BiometricPrompt is unavailable on the device.
|
||||
*/
|
||||
abstract fun showPinDialogWarning(context: Context)
|
||||
|
||||
/**
|
||||
* Toggle preferences to enable or disable navigation during authentication flows.
|
||||
*
|
||||
* @param prefList a list of [Preference]s to toggle.
|
||||
* @param enabled whether or not the preferences should be enabled.
|
||||
*/
|
||||
fun togglePrefsEnabled(prefList: List<Int>, enabled: Boolean) {
|
||||
for (preference in prefList) {
|
||||
requirePreference<Preference>(preference).isEnabled = enabled
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a prompt to verify the device's pin/password and start activity based on the result.
|
||||
* This is only used when BiometricPrompt is unavailable on the device.
|
||||
*/
|
||||
@Suppress("Deprecation")
|
||||
abstract fun showPinVerification(manager: KeyguardManager)
|
||||
|
||||
/**
|
||||
* Sets the biometric prompt feature.
|
||||
*
|
||||
* @param view the view that the prompt will be associate with.
|
||||
* @param prefList a list of [Preference]s to toggle.
|
||||
*/
|
||||
fun setBiometricPrompt(view: View, prefList: List<Int>) {
|
||||
biometricPromptFeature.set(
|
||||
feature = BiometricPromptFeature(
|
||||
context = requireContext(),
|
||||
fragment = this,
|
||||
onAuthFailure = {
|
||||
togglePrefsEnabled(prefList, true)
|
||||
},
|
||||
onAuthSuccess = ::navigateOnSuccess
|
||||
),
|
||||
owner = this,
|
||||
view = view
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Use [BiometricPromptFeature] or [KeyguardManager] to confirm device security.
|
||||
*
|
||||
* @param prefList a list of [Preference]s to disable while authentication is happening.
|
||||
*/
|
||||
fun verifyCredentialsOrShowSetupWarning(context: Context, prefList: List<Int>) {
|
||||
// Use the BiometricPrompt if available
|
||||
if (BiometricPromptFeature.canUseFeature(context)) {
|
||||
togglePrefsEnabled(prefList, false)
|
||||
biometricPromptFeature.get()?.requestAuthentication(unlockMessage())
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to prompting for password with the KeyguardManager
|
||||
val manager = context.getSystemService<KeyguardManager>()
|
||||
if (manager?.isKeyguardSecure == true) {
|
||||
showPinVerification(manager)
|
||||
} else {
|
||||
// Warn that the device has not been secured
|
||||
if (context.settings().shouldShowSecurityPinWarning) {
|
||||
showPinDialogWarning(context)
|
||||
} else {
|
||||
navigateOnSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (requestCode == PIN_REQUEST && resultCode == RESULT_OK) {
|
||||
navigateOnSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PIN_REQUEST = 303
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/* 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.settings.creditcards
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import mozilla.components.support.utils.creditCardIIN
|
||||
|
||||
// Number of last digits to be shown when credit card number is obfuscated.
|
||||
private const val LAST_VISIBLE_DIGITS_COUNT = 4
|
||||
|
||||
/**
|
||||
* Strips characters other than digits from a string.
|
||||
* Used to strip a credit card number user input of spaces and separators.
|
||||
*/
|
||||
fun String.toCreditCardNumber(): String {
|
||||
return this.filter { it.isDigit() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last 4 digits from a formatted credit card number string.
|
||||
*/
|
||||
fun String.last4Digits(): String {
|
||||
return this.takeLast(LAST_VISIBLE_DIGITS_COUNT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the provided string is a valid credit card by checking if it has a matching
|
||||
* credit card issuer network passes the Luhn Algorithm, and false otherwise.
|
||||
*/
|
||||
fun String.validateCreditCardNumber(): Boolean {
|
||||
val creditCardNumber = this.toCreditCardNumber()
|
||||
|
||||
if (creditCardNumber != this || creditCardNumber.creditCardIIN() == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
return luhnAlgorithmValidation(creditCardNumber)
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of Luhn Algorithm validation (https://en.wikipedia.org/wiki/Luhn_algorithm)
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
@VisibleForTesting
|
||||
internal fun luhnAlgorithmValidation(creditCardNumber: String): Boolean {
|
||||
var checksum = 0
|
||||
val reversedCardNumber = creditCardNumber.reversed()
|
||||
|
||||
for (index in reversedCardNumber.indices) {
|
||||
val digit = Character.getNumericValue(reversedCardNumber[index])
|
||||
checksum += if (index % 2 == 0) digit else (digit * 2).let { (it / 10) + (it % 10) }
|
||||
}
|
||||
|
||||
return (checksum % 10) == 0
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue