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