Merge remote-tracking branch 'origin/fenix/123.0' into iceraven

pull/745/head
akliuxingyuan 3 months ago
commit d2da775dfd

@ -82,6 +82,8 @@ android {
"targetActivity": targetActivity, "targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue
] ]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
} }
def releaseTemplate = { def releaseTemplate = {
@ -239,7 +241,14 @@ android {
reset() reset()
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64" // As gradle is unable to pick the right apk to install when multiple apks are generated
// while running benchmark tests or generating baseline profiles. To circumvent this,
// this flag is passed to make sure only one apk is generated so gradle can pick that one.
if (project.hasProperty("benchmarkTest")) {
include "arm64-v8a"
} else {
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64"
}
} }
} }
@ -515,7 +524,6 @@ android.applicationVariants.configureEach { variant ->
} else { } else {
buildConfigField "boolean", "LEAKCANARY", "false" buildConfigField "boolean", "LEAKCANARY", "false"
} }
} }
// Generate Kotlin code for the Fenix Glean metrics. // Generate Kotlin code for the Fenix Glean metrics.
@ -670,13 +678,16 @@ dependencies {
implementation ComponentsDependencies.androidx_fragment implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui implementation FenixDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_compose_navigation
implementation ComponentsDependencies.androidx_recyclerview implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_lifecycle_common implementation ComponentsDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_lifecycle_service
implementation ComponentsDependencies.androidx_core implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx implementation ComponentsDependencies.androidx_core_ktx
implementation FenixDependencies.androidx_core_splashscreen implementation FenixDependencies.androidx_core_splashscreen
@ -837,25 +848,6 @@ tasks.register('printVariants') {
} }
} }
tasks.register('buildTranslationArray') {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString
}
afterEvaluate { afterEvaluate {
// Format test output. Ported from AC #2401 // Format test output. Ported from AC #2401
@ -922,5 +914,24 @@ android.applicationVariants.configureEach { variant ->
} }
} }
def getSupportedLocales() {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
return foundLocalesString
}
// Enable expiration by major version. // Enable expiration by major version.
ext.gleanExpireByVersion = Config.majorVersion() ext.gleanExpireByVersion = Config.majorVersion()

@ -135,12 +135,12 @@ events:
description: | description: |
A string containing the name of the item the user tapped. These items A string containing the name of the item the user tapped. These items
include: include:
add_to_homescreen, add_to_top_sites, addons_manager, back, bookmark, add_to_homescreen, add_to_top_sites, addons_manager, back, back_long_press,
bookmarks, desktop_view_off, desktop_view_on, downloads, bookmark, bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, history, new_tab, open_in_app, open_in_fenix, find_in_page, forward, forward_long_press, history, new_tab, open_in_app,
quit, reader_mode_appearance, reload, remove_from_top_sites, open_in_fenix, quit, reader_mode_appearance, reload, remove_from_top_sites,
save_to_collection, set_default_browser, settings, share, stop, save_to_collection, set_default_browser, settings, share, stop,
sync_account, and print_content. sync_account, translate and print_content.
type: string type: string
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024 - https://github.com/mozilla-mobile/fenix/issues/1024
@ -475,6 +475,23 @@ events:
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: never expires: never
browser_toolbar_security_indicator_tapped:
type: event
description: |
An event that indicates that a user has tapped
the security indicator icon (at the start of the domain name).
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
browser_toolbar_erase_tapped: browser_toolbar_erase_tapped:
type: event type: event
description: | description: |
@ -489,6 +506,22 @@ events:
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: never expires: never
browser_toolbar_input_cleared:
type: event
description: |
A user pressed the circle cross icon, clearing the input in the toolbar.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
browser_toolbar_qr_scan_tapped: browser_toolbar_qr_scan_tapped:
type: event type: event
description: | description: |
@ -506,6 +539,22 @@ events:
metadata: metadata:
tags: tags:
- Toolbar - Toolbar
browser_toolbar_qr_scan_completed:
type: event
description: |
An event that indicates that a QR code has been scanned successfully.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1869664
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/5019#issuecomment-1876329933
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: never
metadata:
tags:
- Toolbar
toolbar_tab_swipe: toolbar_tab_swipe:
type: event type: event
description: | description: |

@ -214,14 +214,6 @@ features:
type: Boolean type: Boolean
default: true default: true
extensions-process:
description: A feature to rollout the extensions process.
variables:
enabled:
description: If true, the extensions process is enabled.
type: Boolean
default: true
growth-data: growth-data:
description: A feature measuring campaign growth data description: A feature measuring campaign growth data
variables: variables:
@ -341,6 +333,26 @@ features:
type: Map<String, String> type: Map<String, String>
default: {} default: {}
fx-strong-password:
description: A feature that provides a generated strong password on sign up.
variables:
enabled:
description: >
When the feature is enabled and Firefox receives a Login event with an
empty saved logins list, a suggested strong password prompt will be shown,
allowing the user to use the generated password to fill in the password field
for the new account that will be created. When the feature is disabled,
there won't be any prompt displayed that would allow using a generated password.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
- channel: nightly
value:
enabled: true
fx-suggest: fx-suggest:
description: A feature that provides Firefox Suggest search suggestions. description: A feature that provides Firefox Suggest search suggestions.
variables: variables:
@ -361,6 +373,17 @@ features:
- channel: nightly - channel: nightly
value: value:
enabled: true enabled: true
nimbus-is-ready:
description: >
A feature that provides the number of Nimbus is_ready events to send
when Nimbus finishes launching.
variables:
event-count:
description: The number of events that should be sent.
type: Int
default: 1
types: types:
objects: {} objects: {}

@ -25,8 +25,7 @@ features:
card-type: default-browser card-type: default-browser
title: juno_onboarding_default_browser_title_nimbus_2 title: juno_onboarding_default_browser_title_nimbus_2
ordering: 10 ordering: 10
body: juno_onboarding_default_browser_description_nimbus_2 body: juno_onboarding_default_browser_description_nimbus_3
link-text: juno_onboarding_default_browser_description_link_text
image-res: ic_onboarding_welcome image-res: ic_onboarding_welcome
primary-button-label: juno_onboarding_default_browser_positive_button primary-button-label: juno_onboarding_default_browser_positive_button
secondary-button-label: juno_onboarding_default_browser_negative_button secondary-button-label: juno_onboarding_default_browser_negative_button
@ -83,13 +82,6 @@ objects:
description: The message text displayed to the user. May contain linkable text. description: The message text displayed to the user. May contain linkable text.
# This should never be defaulted. # This should never be defaulted.
default: "" default: ""
link-text:
type: Option<Text>
description: >
The text to link from the body text. This should match the linkable text from the body text exactly.
e.g. body: This is a policy link
link-text: policy link
default: null
image-res: image-res:
type: Image type: Image
description: The resource id of the image to be displayed. description: The resource id of the image to be displayed.

@ -92,7 +92,6 @@ class AppRequestInterceptor(
// This method is the only difference from the production code. // This method is the only difference from the production code.
// Otherwise the code should be kept identical // Otherwise the code should be kept identical
@Suppress("LongParameterList")
private fun interceptFxaRequest( private fun interceptFxaRequest(
engineSession: EngineSession, engineSession: EngineSession,
uri: String, uri: String,

@ -146,11 +146,12 @@
}, },
"jinja2": { "jinja2": {
"hashes": [ "hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852", "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61" "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
], ],
"index": "pypi",
"markers": "python_version >= '3.7'", "markers": "python_version >= '3.7'",
"version": "==3.1.2" "version": "==3.1.3"
}, },
"markupsafe": { "markupsafe": {
"hashes": [ "hashes": [

@ -6,19 +6,15 @@ package org.mozilla.fenix.extensions
import android.content.Context import android.content.Context
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import org.json.JSONObject
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gecko.GeckoProvider import org.mozilla.fenix.gecko.GeckoProvider
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.nimbus.FxNimbus
/** /**
* Instrumentation test for verifying that the extensions process can be controlled with Nimbus. * Instrumentation test for verifying that the extensions process is enabled unconditionally.
*/ */
class ExtensionProcessTest { class ExtensionProcessTest {
private lateinit var context: Context private lateinit var context: Context
@ -27,49 +23,12 @@ class ExtensionProcessTest {
@Before @Before
fun setUp() { fun setUp() {
context = TestHelper.appContext context = TestHelper.appContext
policy = policy = context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
} }
@Test @Test
fun test_extension_process_can_be_enabled_by_nimbus() { fun test_extension_process_is_enabled() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": true
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
val runtime = GeckoProvider.createRuntimeSettings(context, policy) val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertTrue(FxNimbus.features.extensionsProcess.value().enabled)
assertTrue(runtime.extensionsProcessEnabled!!) assertTrue(runtime.extensionsProcessEnabled!!)
} }
@Test
fun test_extension_process_can_be_disabled_by_nimbus() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": false
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertFalse(FxNimbus.features.extensionsProcess.value().enabled)
assertFalse(runtime.extensionsProcessEnabled!!)
}
} }

@ -249,6 +249,7 @@ object AppAndSystemHelper {
* Runs on Debug variant as we don't want to adjust Release permission manifests * Runs on Debug variant as we don't want to adjust Release permission manifests
* Runs the test in its testBlock. * Runs the test in its testBlock.
* Cleans up and sets the default locale after it's done. * Cleans up and sets the default locale after it's done.
* As a safety measure, always add the resetSystemLocaleToEnUS() method in the tearDown method of your Class.
*/ */
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) { fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
if (Config.channel.isDebug) { if (Config.channel.isDebug) {
@ -274,6 +275,21 @@ object AppAndSystemHelper {
} }
} }
/**
* Resets the default language of the entire device back to EN-US.
* In case of a test instrumentation crash, the finally statement in the
* runWithSystemLocaleChanged(locale: Locale) method, will not be reached.
* Add this method inside the tearDown method of your test class, where the above method is used.
* Note: If set inside the ActivityTestRule's afterActivityFinished() method, this also won't work,
* as the methods inside it are not always executed: https://github.com/android/android-test/issues/498
*/
fun resetSystemLocaleToEnUS() {
if (Locale.getDefault() != Locale.US) {
Log.i(TAG, "Resetting system locale to EN US")
setSystemLocale(Locale.US)
}
}
/** /**
* Changes the default language of the entire device, not just the app. * Changes the default language of the entire device, not just the app.
*/ */

@ -22,6 +22,8 @@ import org.junit.Assert
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.mDevice import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.utils.IntentUtils
import java.time.LocalDate
import java.time.LocalTime
object DataGenerationHelper { object DataGenerationHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
@ -75,6 +77,28 @@ object DataGenerationHelper {
clipBoard.setPrimaryClip(clipData) clipBoard.setPrimaryClip(clipData)
} }
/**
* Constructs a date and time placeholder string for sponsored Fx suggest links.
* The format of the datetime is YYYYMMDDHH, where YYYY is the four-digit year,
* MM is the two-digit month, DD is the two-digit day, and HH is the two-digit hour.
* Single-digit months, days, and hours are padded with a leading zero to ensure
* the correct format. For example, a date and time of January 10, 2024, at 3 PM
* would be represented as "2024011015".
*
* @return A string representing the current date and time in the specified format.
*/
fun getSponsoredFxSuggestPlaceHolder(): String {
val currentDate = LocalDate.now()
val currentTime = LocalTime.now()
val currentDay = currentDate.dayOfMonth.toString().padStart(2, '0')
val currentMonth = currentDate.monthValue.toString().padStart(2, '0')
val currentYear = currentDate.year.toString()
val currentHour = currentTime.hour.toString().padStart(2, '0')
return currentYear + currentMonth + currentDay + currentHour
}
/** /**
* Returns sponsored shortcut title based on the index. * Returns sponsored shortcut title based on the index.
*/ */

@ -82,6 +82,11 @@ interface FeatureSettingsHelper {
*/ */
var composeTopSitesEnabled: Boolean var composeTopSitesEnabled: Boolean
/**
* Enable or disable translations flow.
*/
var isTranslationsEnabled: Boolean
fun applyFlagUpdates() fun applyFlagUpdates()
fun resetAllFeatureFlags() fun resetAllFeatureFlags()

@ -37,6 +37,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
etpPolicy = getETPPolicy(settings), etpPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose, tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
composeTopSitesEnabled = settings.enableComposeTopSites, composeTopSitesEnabled = settings.enableComposeTopSites,
translationsEnabled = settings.enableTranslations,
) )
/** /**
@ -66,6 +67,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled
override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled
override var isTranslationsEnabled: Boolean by updatedFeatureFlags::translationsEnabled
override fun applyFlagUpdates() { override fun applyFlagUpdates() {
applyFeatureFlags(updatedFeatureFlags) applyFeatureFlags(updatedFeatureFlags)
@ -91,6 +93,7 @@ class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled
settings.enableTranslations = featureFlags.translationsEnabled
setETPPolicy(featureFlags.etpPolicy) setETPPolicy(featureFlags.etpPolicy)
} }
} }
@ -110,6 +113,7 @@ private data class FeatureFlags(
var etpPolicy: ETPPolicy, var etpPolicy: ETPPolicy,
var tabsTrayRewriteEnabled: Boolean, var tabsTrayRewriteEnabled: Boolean,
var composeTopSitesEnabled: Boolean, var composeTopSitesEnabled: Boolean,
var translationsEnabled: Boolean,
) )
internal fun getETPPolicy(settings: Settings): ETPPolicy { internal fun getETPPolicy(settings: Settings): ETPPolicy {

@ -165,6 +165,7 @@ class HomeActivityIntentTestRule internal constructor(
etpPolicy: ETPPolicy = getETPPolicy(settings), etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false, tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false, composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) { ) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled this.isPocketEnabled = isPocketEnabled
@ -179,6 +180,7 @@ class HomeActivityIntentTestRule internal constructor(
this.etpPolicy = etpPolicy this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.composeTopSitesEnabled = composeTopSitesEnabled this.composeTopSitesEnabled = composeTopSitesEnabled
this.isTranslationsEnabled = translationsEnabled
} }
private val longTapUserPreference = getLongPressTimeout() private val longTapUserPreference = getLongPressTimeout()
@ -260,6 +262,7 @@ class HomeActivityIntentTestRule internal constructor(
skipOnboarding: Boolean = false, skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false, tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false, composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) = HomeActivityIntentTestRule( ) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode, initialTouchMode = initialTouchMode,
launchActivity = launchActivity, launchActivity = launchActivity,
@ -271,6 +274,7 @@ class HomeActivityIntentTestRule internal constructor(
isWallpaperOnboardingEnabled = false, isWallpaperOnboardingEnabled = false,
isOpenInAppBannerEnabled = false, isOpenInAppBannerEnabled = false,
composeTopSitesEnabled = composeTopSitesEnabled, composeTopSitesEnabled = composeTopSitesEnabled,
translationsEnabled = translationsEnabled,
) )
} }
} }

@ -4,6 +4,7 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.util.Log
import androidx.test.espresso.IdlingResourceTimeoutException import androidx.test.espresso.IdlingResourceTimeoutException
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.uiautomator.UiObjectNotFoundException import androidx.test.uiautomator.UiObjectNotFoundException
@ -13,7 +14,9 @@ import org.junit.rules.TestRule
import org.junit.runner.Description import org.junit.runner.Description
import org.junit.runners.model.Statement import org.junit.runners.model.Statement
import org.mozilla.fenix.components.PermissionStorage import org.mozilla.fenix.components.PermissionStorage
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled import org.mozilla.fenix.helpers.AppAndSystemHelper.setNetworkEnabled
import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources import org.mozilla.fenix.helpers.IdlingResourceHelper.unregisterAllIdlingResources
import org.mozilla.fenix.helpers.TestHelper.appContext import org.mozilla.fenix.helpers.TestHelper.appContext
@ -32,68 +35,83 @@ class RetryTestRule(private val retryCount: Int = 5) : TestRule {
return statement { return statement {
for (i in 1..retryCount) { for (i in 1..retryCount) {
try { try {
Log.i(TAG, "RetryTestRule: Started try #$i.")
base.evaluate() base.evaluate()
break break
} catch (t: AssertionError) { } catch (t: AssertionError) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: AssertionFailedError) { } catch (t: AssertionFailedError) {
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: UiObjectNotFoundException) { } catch (t: UiObjectNotFoundException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: NoMatchingViewException) { } catch (t: NoMatchingViewException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: IdlingResourceTimeoutException) { } catch (t: IdlingResourceTimeoutException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: RuntimeException) { } catch (t: RuntimeException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} catch (t: NullPointerException) { } catch (t: NullPointerException) {
setNetworkEnabled(true) setNetworkEnabled(true)
unregisterAllIdlingResources() unregisterAllIdlingResources()
runBlocking { runBlocking {
appContext.settings().alwaysOpenTheHomepageWhenOpeningTheApp = true
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
if (i == retryCount) { if (i == retryCount) {
Log.i(TAG, "RetryTestRule: Max numbers of retries reached.")
throw t throw t
} }
} }

@ -45,10 +45,12 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() { fun showNotificationTrue_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutAddWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData, notificationPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData, notificationPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true, showNotificationPage = true,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -59,10 +61,12 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() { fun showNotificationFalse_showAddWidgetFalse_pagesToDisplay_returnsSortedListOfConvertedPages_withoutNotificationPage_and_addWidgetPage() {
val expected = listOf(defaultBrowserPageUiData, syncPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, syncPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -71,12 +75,77 @@ class OnboardingMapperTest {
) )
} }
@Test
fun pagesToDisplay_returnsSortedListOfConvertedPages_withPrivacyCaption_alwaysOnFirstPage() {
var result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = true,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true,
showAddWidgetPage = false,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
assertEquals(result[2].privacyCaption, null)
result = unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = true,
jexlConditions = jexlConditions,
func = evalFunction,
)
assertEquals(result[0].privacyCaption, privacyCaption)
assertEquals(result[1].privacyCaption, null)
}
@Test
fun showDefaultBrowserPageFalse_showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages() {
val expected = listOf(addSearchWidgetPageUiDataWithPrivacyCaption, syncPageUiData)
assertEquals(
expected,
unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = false,
showNotificationPage = false,
showAddWidgetPage = true,
jexlConditions = jexlConditions,
func = evalFunction,
),
)
}
@Test @Test
fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() { fun showNotificationFalse_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfAllConvertedPages_withoutNotificationPage() {
val expected = listOf(defaultBrowserPageUiData, addSearchWidgetPageUiData, syncPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption, addSearchWidgetPageUiData, syncPageUiData)
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = true, showAddWidgetPage = true,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -88,7 +157,7 @@ class OnboardingMapperTest {
@Test @Test
fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() { fun showNotificationTrue_and_showAddWidgetTrue_pagesToDisplay_returnsSortedListOfConvertedPages() {
val expected = listOf( val expected = listOf(
defaultBrowserPageUiData, defaultBrowserPageUiDataWithPrivacyCaption,
addSearchWidgetPageUiData, addSearchWidgetPageUiData,
syncPageUiData, syncPageUiData,
notificationPageUiData, notificationPageUiData,
@ -96,6 +165,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
unsortedAllKnownCardData.toPageUiData( unsortedAllKnownCardData.toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = true, showNotificationPage = true,
showAddWidgetPage = true, showAddWidgetPage = true,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -107,11 +178,13 @@ class OnboardingMapperTest {
@Test @Test
fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun cardConditionsMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -128,6 +201,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(addSearchWidgetCardDataNoConditions).toPageUiData( listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -144,6 +219,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -155,11 +232,13 @@ class OnboardingMapperTest {
@Test @Test
fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun prerequisitesMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true") val jexlConditions = mapOf("ALWAYS" to "true")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -176,6 +255,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardData).toPageUiData( listOf(defaultBrowserCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -192,6 +273,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(addSearchWidgetCardDataNoConditions).toPageUiData( listOf(addSearchWidgetCardDataNoConditions).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -203,11 +286,13 @@ class OnboardingMapperTest {
@Test @Test
fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() { fun noDisqualifiers_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(defaultBrowserPageUiData) val expected = listOf(defaultBrowserPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData( listOf(defaultBrowserCardDataNoDisqualifiers).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -219,11 +304,13 @@ class OnboardingMapperTest {
@Test @Test
fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() { fun disqualifiersMatchJexlConditions_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("NEVER" to "false") val jexlConditions = mapOf("NEVER" to "false")
val expected = listOf(syncPageUiData) val expected = listOf(syncPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(syncCardData).toPageUiData( listOf(syncCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -240,6 +327,8 @@ class OnboardingMapperTest {
assertEquals( assertEquals(
expected, expected,
listOf(notificationCardData).toPageUiData( listOf(notificationCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -251,11 +340,13 @@ class OnboardingMapperTest {
@Test @Test
fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() { fun noPrerequisites_shouldDisplayCard_returnsConvertedPage() {
val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false") val jexlConditions = mapOf("ALWAYS" to "true", "NEVER" to "false")
val expected = listOf(syncPageUiData) val expected = listOf(syncPageUiDataWithPrivacyCaption)
assertEquals( assertEquals(
expected, expected,
listOf(syncCardData).toPageUiData( listOf(syncCardData).toPageUiData(
privacyCaption = privacyCaption,
showDefaultBrowserPage = true,
showNotificationPage = false, showNotificationPage = false,
showAddWidgetPage = false, showAddWidgetPage = false,
jexlConditions = jexlConditions, jexlConditions = jexlConditions,
@ -264,24 +355,34 @@ class OnboardingMapperTest {
) )
} }
} }
val privacyCaption: Caption = mockk(relaxed = true)
private val defaultBrowserPageUiData = OnboardingPageUiData( private val defaultBrowserPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER, type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = "default browser title", title = "default browser title",
description = "default browser body with link text", description = "default browser body",
linkText = "link text",
primaryButtonLabel = "default browser primary button text", primaryButtonLabel = "default browser primary button text",
secondaryButtonLabel = "default browser secondary button text", secondaryButtonLabel = "default browser secondary button text",
privacyCaption = privacyCaption,
) )
private val addSearchWidgetPageUiData = OnboardingPageUiData( private val addSearchWidgetPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET, type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title", title = "add search widget title",
description = "add search widget body with link text", description = "add search widget body",
linkText = "link text", primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text",
privacyCaption = null,
)
private val addSearchWidgetPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget,
title = "add search widget title",
description = "add search widget body",
primaryButtonLabel = "add search widget primary button text", primaryButtonLabel = "add search widget primary button text",
secondaryButtonLabel = "add search widget secondary button text", secondaryButtonLabel = "add search widget secondary button text",
privacyCaption = privacyCaption,
) )
private val syncPageUiData = OnboardingPageUiData( private val syncPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN, type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
@ -290,6 +391,16 @@ private val syncPageUiData = OnboardingPageUiData(
description = "sync body", description = "sync body",
primaryButtonLabel = "sync primary button text", primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text", secondaryButtonLabel = "sync secondary button text",
privacyCaption = null,
)
private val syncPageUiDataWithPrivacyCaption = OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
imageRes = R.drawable.ic_onboarding_sync,
title = "sync title",
description = "sync body",
primaryButtonLabel = "sync primary button text",
secondaryButtonLabel = "sync secondary button text",
privacyCaption = privacyCaption,
) )
private val notificationPageUiData = OnboardingPageUiData( private val notificationPageUiData = OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION, type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
@ -298,14 +409,14 @@ private val notificationPageUiData = OnboardingPageUiData(
description = "notification body", description = "notification body",
primaryButtonLabel = "notification primary button text", primaryButtonLabel = "notification primary button text",
secondaryButtonLabel = "notification secondary button text", secondaryButtonLabel = "notification secondary button text",
privacyCaption = null,
) )
private val defaultBrowserCardData = OnboardingCardData( private val defaultBrowserCardData = OnboardingCardData(
cardType = OnboardingCardType.DEFAULT_BROWSER, cardType = OnboardingCardType.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = StringHolder(null, "default browser title"), title = StringHolder(null, "default browser title"),
body = StringHolder(null, "default browser body with link text"), body = StringHolder(null, "default browser body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "default browser primary button text"), primaryButtonLabel = StringHolder(null, "default browser primary button text"),
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10, ordering = 10,
@ -317,8 +428,7 @@ private val defaultBrowserCardDataNoDisqualifiers = OnboardingCardData(
cardType = OnboardingCardType.DEFAULT_BROWSER, cardType = OnboardingCardType.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = StringHolder(null, "default browser title"), title = StringHolder(null, "default browser title"),
body = StringHolder(null, "default browser body with link text"), body = StringHolder(null, "default browser body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "default browser primary button text"), primaryButtonLabel = StringHolder(null, "default browser primary button text"),
secondaryButtonLabel = StringHolder(null, "default browser secondary button text"), secondaryButtonLabel = StringHolder(null, "default browser secondary button text"),
ordering = 10, ordering = 10,
@ -330,8 +440,7 @@ private val addSearchWidgetCardDataNoConditions = OnboardingCardData(
cardType = OnboardingCardType.ADD_SEARCH_WIDGET, cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = StringHolder(null, "add search widget title"), title = StringHolder(null, "add search widget title"),
body = StringHolder(null, "add search widget body with link text"), body = StringHolder(null, "add search widget body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "add search widget primary button text"), primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
ordering = 15, ordering = 15,
@ -343,8 +452,7 @@ private val addSearchWidgetCardData = OnboardingCardData(
cardType = OnboardingCardType.ADD_SEARCH_WIDGET, cardType = OnboardingCardType.ADD_SEARCH_WIDGET,
imageRes = R.drawable.ic_onboarding_search_widget, imageRes = R.drawable.ic_onboarding_search_widget,
title = StringHolder(null, "add search widget title"), title = StringHolder(null, "add search widget title"),
body = StringHolder(null, "add search widget body with link text"), body = StringHolder(null, "add search widget body"),
linkText = StringHolder(null, "link text"),
primaryButtonLabel = StringHolder(null, "add search widget primary button text"), primaryButtonLabel = StringHolder(null, "add search widget primary button text"),
secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"), secondaryButtonLabel = StringHolder(null, "add search widget secondary button text"),
ordering = 15, ordering = 15,

@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -265,7 +264,6 @@ class AddressAutofillTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836849 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1836849
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1814032")
@Test @Test
fun verifyMultipleAddressesSelectionTest() { fun verifyMultipleAddressesSelectionTest() {
val addressFormPage = val addressFormPage =

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper.resetSystemLocaleToEnUS
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -54,6 +55,7 @@ class ComposeNavigationToolbarTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326

@ -0,0 +1,54 @@
/* 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
import androidx.core.net.toUri
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying the new Cookie banner blocker option and functionality.
*/
class CookieBannerBlockerTest {
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides(skipOnboarding = true)
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419260
@SmokeTest
@Test
fun verifyCookieBannerBlockerSettingsOptionTest() {
runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyCookieBannerBlockerButton(enabled = true)
}
}
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2419273
@SmokeTest
@Test
fun verifyCFRAfterBlockingTheCookieBanner() {
runWithCondition(appContext.settings().shouldUseCookieBannerPrivateMode) {
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser("voetbal24.be".toUri()) {
waitForPageToLoad()
verifyCookieBannerExists(exists = false)
verifyCookieBannerBlockerCFRExists(exists = true)
}
}
}
}

@ -5,11 +5,13 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AppAndSystemHelper import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithCondition
import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredFxSuggestPlaceHolder
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -20,6 +22,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/ */
class FirefoxSuggestTest { class FirefoxSuggestTest {
@get:Rule @get:Rule
val activityTestRule = AndroidComposeTestRule( val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule( HomeActivityTestRule(
@ -33,91 +36,156 @@ class FirefoxSuggestTest {
), ),
) { it.activity } ) { it.activity }
private val sponsoredKeyWords: Map<String, List<String>> =
mapOf(
"Amazon" to
listOf(
"Amazon.com - Official Site",
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
),
"Nike" to
listOf(
"Nike.com - Official Site",
"nike.com/?cp=16423867261_search_318370984us128${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Macy" to listOf(
"macys.com - Official Site",
"macys.com/?cm_mmc=Google_AdMarketPlace-_-Privacy_Instant%20Suggest-_-319101130_Broad-_-kclickid__kenshoo_clickid_&m_sc=sem&m_sb=Admarketplace&m_tp=Search&m_ac=Admarketplace&m_ag=Instant%20Suggest&m_cn=Privacy&m_pi=kclickid__kenshoo_clickid__319101130us1201${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Spanx" to listOf(
"SPANX® - Official Site",
"spanx.com/?utm_source=admarketplace&utm_medium=cpc&utm_campaign=privacy&utm_content=319093361us1202${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
"Bloom" to listOf(
"Bloomingdales.com - Official Site",
"bloomingdales.com/?cm_mmc=Admarketplace-_-Privacy-_-Privacy-_-privacy%20instant%20suggest-_-319093353us1228${getSponsoredFxSuggestPlaceHolder()}-_-kclickid__kenshoo_clickid_&mfadid=adm",
),
"Groupon" to listOf(
"groupon.com - Discover & Save!",
"groupon.com/?utm_source=google&utm_medium=cpc&utm_campaign=us_dt_sea_ggl_txt_smp_sr_cbp_ch1_nbr_k*{keyword}_m*{match-type}_d*ADMRKT_319093357us1279${getSponsoredFxSuggestPlaceHolder()}&mfadid=adm",
),
)
private val sponsoredKeyWord = sponsoredKeyWords.keys.random()
private val nonSponsoredKeyWords: Map<String, List<String>> =
mapOf(
"Marvel" to
listOf(
"Wikipedia - Marvel Cinematic Universe",
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
),
"Apple" to
listOf(
"Wikipedia - Apple Inc.",
"wikipedia.org/wiki/Apple_Inc",
),
"Africa" to listOf(
"Wikipedia - African Union",
"wikipedia.org/wiki/African_Union",
),
"Ultimate" to listOf(
"Wikipedia - Ultimate Fighting Championship",
"wikipedia.org/wiki/Ultimate_Fighting_Championship",
),
"Youtube" to listOf(
"Wikipedia - YouTube",
"wikipedia.org/wiki/YouTube",
),
"Fifa" to listOf(
"Wikipedia - FIFA World Cup",
"en.m.wikipedia.org/wiki/FIFA_World_Cup",
),
)
private val nonSponsoredKeyWord = nonSponsoredKeyWords.keys.random()
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348361
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@SmokeTest @SmokeTest
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsTest() { fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
) )
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348362
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() { fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amaz") typeSearch(searchTerm = sponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amaz", searchTerm = sponsoredKeyWord.dropLast(1),
) )
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348363
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun openFirefoxSuggestSponsoredSearchResultsTest() { fun openFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
)
}.clickSearchSuggestion("Amazon.com - Official Site") {
waitForPageToLoad()
verifyUrl(
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
) )
}.clickSearchSuggestion(sponsoredKeyWords.getValue(sponsoredKeyWord)[0]) {
verifyUrl(sponsoredKeyWords.getValue(sponsoredKeyWord)[1])
verifyTabCounter("1") verifyTabCounter("1")
} }
} }
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348369
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@Test @Test
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() { fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Amazon") typeSearch(searchTerm = sponsoredKeyWord)
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3) deleteSearchKeywordCharacters(numberOfDeletionSteps = 1)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Amazon.com - Official Site", sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored", "Sponsored",
), ),
searchTerm = "Amazon", searchTerm = sponsoredKeyWord,
shouldEditKeyword = true, shouldEditKeyword = true,
numberOfDeletionSteps = 3, numberOfDeletionSteps = 1,
) )
} }
} }
@ -127,17 +195,17 @@ class FirefoxSuggestTest {
@SmokeTest @SmokeTest
@Test @Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() { fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marvel") typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marvel", searchTerm = nonSponsoredKeyWord,
) )
verifySuggestionsAreNotDisplayed( verifySuggestionsAreNotDisplayed(
rule = activityTestRule, rule = activityTestRule,
@ -152,17 +220,17 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
@Test @Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() { fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marv") typeSearch(searchTerm = nonSponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marv", searchTerm = nonSponsoredKeyWord.dropLast(1),
) )
} }
} }
@ -171,23 +239,21 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348376
@Test @Test
fun openFirefoxSuggestNonSponsoredSearchResultsTest() { fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) { runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar { navigationToolbar {
}.clickUrlbar { }.clickUrlbar {
typeSearch(searchTerm = "Marvel") typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
rule = activityTestRule, rule = activityTestRule,
searchSuggestions = arrayOf( searchSuggestions = arrayOf(
"Firefox Suggest", "Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe", nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
), ),
searchTerm = "Marvel", searchTerm = nonSponsoredKeyWord,
) )
}.clickSearchSuggestion("Wikipedia - Marvel Cinematic Universe") { }.clickSearchSuggestion(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0]) {
waitForPageToLoad() waitForPageToLoad()
verifyUrl( verifyUrl(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[1])
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
)
} }
} }
} }

@ -607,14 +607,12 @@ class LoginsTest {
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("android") searchLogin("android")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("AnDrOiD") searchLogin("AnDrOiD")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
@ -654,14 +652,12 @@ class LoginsTest {
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("mozilla") searchLogin("mozilla")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")
revealPassword() revealPassword()
verifyPasswordSaved("firefox") verifyPasswordSaved("firefox")
}.goBackToSavedLogins { }.goBackToSavedLogins {
clickSearchLoginButton()
searchLogin("MoZiLlA") searchLogin("MoZiLlA")
viewSavedLoginDetails(originWebsite) viewSavedLoginDetails(originWebsite)
verifyLoginItemUsername("android") verifyLoginItemUsername("android")

@ -37,7 +37,8 @@ class MainMenuTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
@get:Rule @get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() val activityTestRule =
HomeActivityIntentTestRule.withDefaultSettingsOverrides(translationsEnabled = true)
@Before @Before
fun setUp() { fun setUp() {

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -50,6 +51,7 @@ class NavigationToolbarTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/987326

@ -21,6 +21,7 @@ import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
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.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens import org.mozilla.fenix.helpers.AppAndSystemHelper.assertNativeAppOpens
import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission import org.mozilla.fenix.helpers.AppAndSystemHelper.denyPermission
import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
@ -92,6 +93,7 @@ class SearchTest {
@After @After
fun tearDown() { fun tearDown() {
searchMockServer.shutdown() searchMockServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189

@ -327,7 +327,7 @@ class SettingsAdvancedTest {
} }
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(youTubePage) { }.enterURLAndEnterToBrowser("https://m.youtube.com/".toUri()) {
waitForPageToLoad() waitForPageToLoad()
verifyOpenLinksInAppsCFRExists(true) verifyOpenLinksInAppsCFRExists(true)
}.clickOpenLinksInAppsGoToSettingsCFRButton { }.clickOpenLinksInAppsGoToSettingsCFRButton {

@ -14,6 +14,7 @@ import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.AppAndSystemHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -51,6 +52,7 @@ class SettingsGeneralTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092697 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2092697

@ -5,14 +5,12 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.test.espresso.Espresso.pressBack
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestHelper.exitMenu import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -182,13 +180,7 @@ class SettingsHTTPSOnlyModeTest {
waitForPageToLoad() waitForPageToLoad()
}.openNavigationToolbar { }.openNavigationToolbar {
verifyUrl(httpsPageUrl) verifyUrl(httpsPageUrl)
pressBack() }.goBackToBrowserScreen {
}
browserScreen {
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openHttpsOnlyModeMenu { }.openHttpsOnlyModeMenu {
@ -203,7 +195,6 @@ class SettingsHTTPSOnlyModeTest {
waitForPageToLoad() waitForPageToLoad()
}.openNavigationToolbar { }.openNavigationToolbar {
verifyUrl(httpPageUrl) verifyUrl(httpPageUrl)
pressBack()
} }
} }
} }

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper.resetSystemLocaleToEnUS
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale import org.mozilla.fenix.helpers.AppAndSystemHelper.setSystemLocale
import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard
@ -64,6 +65,7 @@ class SettingsSearchTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
resetSystemLocaleToEnUS()
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333
@ -424,6 +426,10 @@ class SettingsSearchTest {
fun verifyShowSearchSuggestionsToggleTest() { fun verifyShowSearchSuggestionsToggleTest() {
homeScreen { homeScreen {
}.openSearch { }.openSearch {
// The Google related suggestions aren't always displayed on cold run
// Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
clickSearchSelectorButton()
selectTemporarySearchMethod("DuckDuckGo")
typeSearch("mozilla ") typeSearch("mozilla ")
verifySearchEngineSuggestionResults( verifySearchEngineSuggestionResults(
activityTestRule, activityTestRule,
@ -438,6 +444,10 @@ class SettingsSearchTest {
}.goBack { }.goBack {
}.goBack { }.goBack {
}.openSearch { }.openSearch {
// The Google related suggestions aren't always displayed on cold run
// Bugzilla ticket: https://bugzilla.mozilla.org/show_bug.cgi?id=1813587
clickSearchSelectorButton()
selectTemporarySearchMethod("DuckDuckGo")
typeSearch("mozilla") typeSearch("mozilla")
verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox") verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
} }

@ -9,7 +9,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
@ -90,7 +89,6 @@ class SponsoredShortcutsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729335 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729335
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926")
@Test @Test
fun openSponsorsAndYourPrivacyOptionTest() { fun openSponsorsAndYourPrivacyOptionTest() {
homeScreen { homeScreen {
@ -102,7 +100,6 @@ class SponsoredShortcutsTest {
} }
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729336 // TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/1729336
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1807268")
@Test @Test
fun openSponsoredShortcutsSettingsOptionTest() { fun openSponsoredShortcutsSettingsOptionTest() {
homeScreen { homeScreen {

@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.contrib.PickerActions
import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.RootMatchers.isDialog
@ -40,7 +41,6 @@ import org.junit.Assert.assertTrue
import org.junit.Assert.fail import org.junit.Assert.fail
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -686,24 +686,29 @@ class BrowserRobot {
fun verifyCookieBannerExists(exists: Boolean) { fun verifyCookieBannerExists(exists: Boolean) {
for (i in 1..RETRY_COUNT) { for (i in 1..RETRY_COUNT) {
Log.i(TAG, "verifyCookieBannerExists: For loop: $i")
try { try {
assertUIObjectExists(cookieBanner(), exists = exists) // Wait for the blocker to kick-in and make the cookie banner disappear
itemWithResId("CybotCookiebotDialog").waitUntilGone(waitingTime)
Log.i(TAG, "verifyCookieBannerExists: Waiting for window update")
// Assert that the blocker properly dismissed the cookie banner
assertUIObjectExists(itemWithResId("CybotCookiebotDialog"), exists = exists)
break break
} catch (e: AssertionError) { } catch (e: AssertionError) {
if (i == RETRY_COUNT) { if (i == RETRY_COUNT) {
throw e throw e
} else {
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
waitForPageToLoad()
}
} }
} }
} }
assertUIObjectExists(cookieBanner(), exists = exists)
} }
fun verifyCookieBannerBlockerCFRExists(exists: Boolean) =
assertUIObjectExists(
itemContainingText(getStringResource(R.string.cookie_banner_cfr_message)),
exists = exists,
)
fun verifyOpenLinkInAnotherAppPrompt() { fun verifyOpenLinkInAnotherAppPrompt() {
assertUIObjectExists( assertUIObjectExists(
itemWithResId("$packageName:id/parentPanel"), itemWithResId("$packageName:id/parentPanel"),
@ -841,7 +846,7 @@ class BrowserRobot {
button.click() button.click()
} }
fun longClickToolbar() = mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")).click(LONG_CLICK_DURATION) fun longClickToolbar() = onView(withId(R.id.mozac_browser_toolbar_url_view)).perform(longClick())
fun verifyDownloadPromptIsDismissed() = fun verifyDownloadPromptIsDismissed() =
assertUIObjectExists( assertUIObjectExists(
@ -1149,6 +1154,7 @@ class BrowserRobot {
"$packageName:id/action", "$packageName:id/action",
getStringResource(R.string.open_in_app_cfr_positive_button_text), getStringResource(R.string.open_in_app_cfr_positive_button_text),
).clickAndWaitForNewWindow(waitingTime) ).clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Clicked \"Go to settings\" open links in apps CFR button")
SettingsRobot().interact() SettingsRobot().interact()
return SettingsRobot.Transition() return SettingsRobot.Transition()
@ -1302,8 +1308,6 @@ fun clearTextFieldItem(item: UiObject) {
item.clearTextField() item.clearTextField()
} }
private fun cookieBanner() = itemWithResId("startsiden-gdpr-disclaimer")
// Context menu items // Context menu items
// Link URL // Link URL
private fun contextMenuLinkUrl(linkUrl: String) = private fun contextMenuLinkUrl(linkUrl: String) =

@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -51,7 +52,6 @@ import org.junit.Assert
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES import org.mozilla.fenix.helpers.Constants.LISTS_MAXSWIPES
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.TAG import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
@ -86,9 +86,9 @@ class HomeScreenRobot {
" service provider, it makes it easier to keep what you do online private from anyone" + " service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device." " else who uses this device."
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar) fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar())
fun verifyHomeScreen() = assertUIObjectExists(homeScreen) fun verifyHomeScreen() = assertUIObjectExists(homeScreen())
fun verifyPrivateBrowsingHomeScreenItems() { fun verifyPrivateBrowsingHomeScreenItems() {
verifyHomeScreenAppBarItems() verifyHomeScreenAppBarItems()
@ -97,19 +97,19 @@ class HomeScreenRobot {
} }
fun verifyHomeScreenAppBarItems() = fun verifyHomeScreenAppBarItems() =
assertUIObjectExists(homeScreen, privateBrowsingButton, homepageWordmark) assertUIObjectExists(homeScreen(), privateBrowsingButton(), homepageWordmark())
fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") = fun verifyNavigationToolbarItems(numberOfOpenTabs: String = "0") =
assertUIObjectExists(navigationToolbar, menuButton, tabCounter(numberOfOpenTabs)) assertUIObjectExists(navigationToolbar(), menuButton, tabCounter(numberOfOpenTabs))
fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton) fun verifyHomePrivateBrowsingButton() = assertUIObjectExists(privateBrowsingButton())
fun verifyHomeMenuButton() = assertUIObjectExists(menuButton) fun verifyHomeMenuButton() = assertUIObjectExists(menuButton)
fun verifyTabButton() = assertTabButton() fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText() fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() { fun verifyHomeWordmark() {
homeScreenList().scrollToBeginning(3) homeScreenList().scrollToBeginning(3)
assertUIObjectExists(homepageWordmark) assertUIObjectExists(homepageWordmark())
} }
fun verifyHomeComponent() = assertHomeComponent() fun verifyHomeComponent() = assertHomeComponent()
@ -140,7 +140,7 @@ class HomeScreenRobot {
).assertExists() ).assertExists()
it.onNodeWithText( it.onNodeWithText(
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_2), getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
).assertExists() ).assertExists()
it.onNodeWithText( it.onNodeWithText(
@ -292,7 +292,7 @@ class HomeScreenRobot {
mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime) mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
} }
fun clickFirefoxLogo() = homepageWordmark.click() fun clickFirefoxLogo() = homepageWordmark().click()
fun verifyThoughtProvokingStories(enabled: Boolean) { fun verifyThoughtProvokingStories(enabled: Boolean) {
if (enabled) { if (enabled) {
@ -481,8 +481,8 @@ class HomeScreenRobot {
} }
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar.waitForExists(waitingTime) navigationToolbar().waitForExists(waitingTime)
navigationToolbar.click() navigationToolbar().click()
mDevice.waitForIdle() mDevice.waitForIdle()
SearchRobot().interact() SearchRobot().interact()
@ -502,14 +502,14 @@ class HomeScreenRobot {
fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) { fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) {
// Switch to private browsing homescreen // Switch to private browsing homescreen
if (switchPBModeOn && !isPrivateModeEnabled()) { if (switchPBModeOn && !isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime) privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton.click() privateBrowsingButton().click()
} }
// Switch to normal browsing homescreen // Switch to normal browsing homescreen
if (!switchPBModeOn && isPrivateModeEnabled()) { if (!switchPBModeOn && isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime) privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton.click() privateBrowsingButton().click()
} }
} }
@ -521,7 +521,7 @@ class HomeScreenRobot {
waitingTime, waitingTime,
) )
privateBrowsingButton.click() privateBrowsingButton().click()
} }
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
@ -535,7 +535,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime) .waitForExists(waitingTime)
navigationToolbar.click() navigationToolbar().click()
NavigationToolbarRobot().interact() NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition() return NavigationToolbarRobot.Transition()
@ -557,7 +557,8 @@ class HomeScreenRobot {
} }
fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition { fun openContextMenuOnSponsoredShortcut(sponsoredShortcutTitle: String, interact: HomeScreenRobot.() -> Unit): Transition {
sponsoredShortcut(sponsoredShortcutTitle).click(LONG_CLICK_DURATION) sponsoredShortcut(sponsoredShortcutTitle).perform(longClick())
Log.i(TAG, "openContextMenuOnSponsoredShortcut: Long clicked to open context menu for $sponsoredShortcutTitle sponsored shortcut")
HomeScreenRobot().interact() HomeScreenRobot().interact()
return Transition() return Transition()
@ -631,8 +632,10 @@ class HomeScreenRobot {
} }
fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition { fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Looking for: ${sponsoredShortcutsSettingsButton.selector}")
sponsoredShortcutsSettingsButton.waitForExists(waitingTime) sponsoredShortcutsSettingsButton.waitForExists(waitingTime)
sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime) sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Clicked ${sponsoredShortcutsSettingsButton.selector} and waiting for $waitingTime for a new window")
SettingsSubMenuHomepageRobot().interact() SettingsSubMenuHomepageRobot().interact()
return SettingsSubMenuHomepageRobot.Transition() return SettingsSubMenuHomepageRobot.Transition()
@ -939,18 +942,19 @@ private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collec
private fun tabsCounter() = onView(withId(R.id.tab_button)) private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun sponsoredShortcut(sponsoredShortcutTitle: String) = private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
mDevice.findObject( onView(
By allOf(
.res("$packageName:id/top_site_title") withId(R.id.top_site_title),
.textContains(sponsoredShortcutTitle), withText(sponsoredShortcutTitle),
),
) )
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) = private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1) composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private val homeScreen = private fun homeScreen() =
itemWithResId("$packageName:id/homeLayout") itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton = private fun privateBrowsingButton() =
itemWithResId("$packageName:id/privateBrowsingButton") itemWithResId("$packageName:id/privateBrowsingButton")
private fun isPrivateModeEnabled(): Boolean = private fun isPrivateModeEnabled(): Boolean =
@ -959,10 +963,10 @@ private fun isPrivateModeEnabled(): Boolean =
"Disable private browsing", "Disable private browsing",
).exists() ).exists()
private val homepageWordmark = private fun homepageWordmark() =
itemWithResId("$packageName:id/wordmark") itemWithResId("$packageName:id/wordmark")
private val navigationToolbar = private fun navigationToolbar() =
itemWithResId("$packageName:id/toolbar") itemWithResId("$packageName:id/toolbar")
private val menuButton = private val menuButton =
itemWithResId("$packageName:id/menuButton") itemWithResId("$packageName:id/menuButton")

@ -15,6 +15,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
@ -43,7 +44,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -149,7 +149,7 @@ class NavigationToolbarRobot {
assertTrue( assertTrue(
itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) || itemWithResId("$packageName:id/browserLayout").waitForExists(waitingTime) ||
itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) || itemWithResId("$packageName:id/download_button").waitForExists(waitingTime) ||
itemWithText(getStringResource(R.string.tcp_cfr_message)).waitForExists(waitingTime), itemWithResId("cfr.dismiss").waitForExists(waitingTime),
) )
} }
@ -272,9 +272,19 @@ class NavigationToolbarRobot {
return HomeScreenRobot.Transition() return HomeScreenRobot.Transition()
} }
fun goBackToBrowserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
Log.i(TAG, "goBackToBrowserScreen: Dismiss awesome bar using device back button")
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
Log.i(TAG, "goBackToBrowserScreen: Waited $waitingTimeShort for window update")
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition { fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): Transition {
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root"))) mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/counter_root")))
tabsCounter().click(LONG_CLICK_DURATION) tabsCounter().perform(longClick())
Log.i(TAG, "Tabs counter long-click successful.") Log.i(TAG, "Tabs counter long-click successful.")
NavigationToolbarRobot().interact() NavigationToolbarRobot().interact()
@ -388,8 +398,7 @@ private fun awesomeBar() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")) mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu)) private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu))
private fun tabTrayButton() = onView(withId(R.id.tab_button)) private fun tabTrayButton() = onView(withId(R.id.tab_button))
private fun tabsCounter() = private fun tabsCounter() = onView(withId(R.id.mozac_browser_toolbar_browser_actions))
mDevice.findObject(By.res("$packageName:id/counter_root"))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard)) private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view") private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
private fun readerViewToggle() = private fun readerViewToggle() =

@ -107,6 +107,27 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading() fun verifyPrivacyHeading() = assertPrivacyHeading()
fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton() fun verifyHTTPSOnlyModeButton() = assertHTTPSOnlyModeButton()
fun verifyCookieBannerBlockerButton(enabled: Boolean) {
scrollToElementByText(getStringResource(R.string.preferences_cookie_banner_reduction_private_mode))
onView(withText(R.string.preferences_cookie_banner_reduction_private_mode))
.check(
matches(
hasCousin(
CoreMatchers.allOf(
withClassName(endsWith("Switch")),
if (enabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
Log.i(TAG, "verifyCookieBannerBlockerButton: Verified if cookie banner blocker toggle is enabled: $enabled")
}
fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton() fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton()
fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton() fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton() fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
@ -583,6 +604,7 @@ private fun assertOpenLinksInAppsButton() {
scrollToElementByText("Open links in apps") scrollToElementByText("Open links in apps")
openLinksInAppsButton() openLinksInAppsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Verified \"Open links in apps\" setting option")
} }
// ADVANCED SECTION // ADVANCED SECTION

@ -124,7 +124,8 @@ private fun assertCurrentTimestamp() {
private fun assertWhatIsNewInFirefoxPreview() { private fun assertWhatIsNewInFirefoxPreview() {
aboutMenuList.scrollToEnd(LISTS_MAXSWIPES) aboutMenuList.scrollToEnd(LISTS_MAXSWIPES)
onView(withText("Whats new in $appName")) val firefox = TestHelper.appContext.getString(R.string.firefox)
onView(withText("Whats new in $firefox"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click()) .perform(click())
} }

@ -4,6 +4,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.util.Log
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
@ -21,6 +22,7 @@ import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers import org.hamcrest.Matchers
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists import org.mozilla.fenix.helpers.MatcherHelper.assertUIObjectExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -61,6 +63,7 @@ class SettingsSubMenuHomepageRobot {
assertHomepageButton() assertHomepageButton()
assertLastTabButton() assertLastTabButton()
assertHomepageAfterFourHoursButton() assertHomepageAfterFourHoursButton()
Log.i(Constants.TAG, "verifyHomePageView: Verified the home page elements")
} }
fun verifySelectedOpeningScreenOption(openingScreenOption: String) = fun verifySelectedOpeningScreenOption(openingScreenOption: String) =

@ -118,7 +118,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
) )
fun searchLogin(searchTerm: String) = fun searchLogin(searchTerm: String) =
itemContainingText(getStringResource(R.string.preferences_passwords_saved_logins_search)).setText(searchTerm) itemWithResId("$packageName:id/search").setText(searchTerm)
fun verifySavedLoginsSectionUsername(username: String) = fun verifySavedLoginsSectionUsername(username: String) =
mDevice.waitNotNull(Until.findObjects(By.text(username))) mDevice.waitNotNull(Until.findObjects(By.text(username)))

@ -54,9 +54,9 @@ import org.mozilla.fenix.nimbus.FxNimbus
class ThreeDotMenuMainRobot { class ThreeDotMenuMainRobot {
fun verifyShareAllTabsButton() = assertShareAllTabsButton() fun verifyShareAllTabsButton() = assertShareAllTabsButton()
fun verifySettingsButton() = assertUIObjectExists(settingsButton()) fun verifySettingsButton() = assertUIObjectExists(settingsButton())
fun verifyHistoryButton() = assertUIObjectExists(historyButton) fun verifyHistoryButton() = assertUIObjectExists(historyButton())
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton) fun verifyAddBookmarkButton() = assertUIObjectExists(addBookmarkButton())
fun verifyEditBookmarkButton() = assertEditBookmarkButton() fun verifyEditBookmarkButton() = assertEditBookmarkButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
@ -76,9 +76,9 @@ class ThreeDotMenuMainRobot {
fun verifyShareTabButton() = assertShareTabButton() fun verifyShareTabButton() = assertShareTabButton()
fun verifySelectTabs() = assertSelectTabsButton() fun verifySelectTabs() = assertSelectTabsButton()
fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton) fun verifyFindInPageButton() = assertUIObjectExists(findInPageButton())
fun verifyAddToShortcutsButton(shouldExist: Boolean) = fun verifyAddToShortcutsButton(shouldExist: Boolean) =
assertUIObjectExists(addToShortcutsButton, exists = shouldExist) assertUIObjectExists(addToShortcutsButton(), exists = shouldExist)
fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton() fun verifyRemoveFromShortcutsButton() = assertRemoveFromShortcutsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyShareTabsOverlay() = assertShareTabsOverlay()
@ -90,20 +90,21 @@ class ThreeDotMenuMainRobot {
fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { fun verifyPageThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
expandMenu() expandMenu()
assertUIObjectExists( assertUIObjectExists(
normalBrowsingNewTabButton, normalBrowsingNewTabButton(),
bookmarksButton, bookmarksButton(),
historyButton, historyButton(),
downloadsButton, downloadsButton(),
addOnsButton, addOnsButton(),
syncAndSaveDataButton, syncAndSaveDataButton(),
findInPageButton, findInPageButton(),
desktopSiteButton, desktopSiteButton(),
reportSiteIssueButton, reportSiteIssueButton(),
addToHomeScreenButton, addToHomeScreenButton(),
addToShortcutsButton, addToShortcutsButton(),
saveToCollectionButton, saveToCollectionButton(),
addBookmarkButton, addBookmarkButton(),
desktopSiteToggle(isRequestDesktopSiteEnabled), desktopSiteToggle(isRequestDesktopSiteEnabled),
translateButton(),
) )
// Swipe to second part of menu // Swipe to second part of menu
expandMenu() expandMenu()
@ -111,28 +112,28 @@ class ThreeDotMenuMainRobot {
settingsButton(), settingsButton(),
) )
if (FxNimbus.features.print.value().browserPrintEnabled) { if (FxNimbus.features.print.value().browserPrintEnabled) {
assertUIObjectExists(printContentButton) assertUIObjectExists(printContentButton())
} }
assertUIObjectExists( assertUIObjectExists(
backButton, backButton(),
forwardButton, forwardButton(),
shareButton, shareButton(),
refreshButton, refreshButton(),
) )
} }
fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) { fun verifyHomeThreeDotMainMenuItems(isRequestDesktopSiteEnabled: Boolean) {
assertUIObjectExists( assertUIObjectExists(
bookmarksButton, bookmarksButton(),
historyButton, historyButton(),
downloadsButton, downloadsButton(),
addOnsButton, addOnsButton(),
// Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788 // Disabled step due to https://github.com/mozilla-mobile/fenix/issues/26788
// syncAndSaveDataButton, // syncAndSaveDataButton,
desktopSiteButton, desktopSiteButton(),
whatsNewButton, whatsNewButton(),
helpButton, helpButton(),
customizeHomeButton, customizeHomeButton(),
settingsButton(), settingsButton(),
desktopSiteToggle(isRequestDesktopSiteEnabled), desktopSiteToggle(isRequestDesktopSiteEnabled),
) )
@ -202,7 +203,7 @@ class ThreeDotMenuMainRobot {
fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition { fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
Log.i(TAG, "openDownloadsManager: Swiped up main menu") Log.i(TAG, "openDownloadsManager: Swiped up main menu")
downloadsButton.click() downloadsButton().click()
Log.i(TAG, "openDownloadsManager: Clicked main menu \"DOWNLOADS\" button") Log.i(TAG, "openDownloadsManager: Clicked main menu \"DOWNLOADS\" button")
DownloadRobot().interact() DownloadRobot().interact()
@ -212,7 +213,7 @@ class ThreeDotMenuMainRobot {
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition { fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Sync and save data")), waitingTime)
syncAndSaveDataButton.click() syncAndSaveDataButton().click()
SyncSignInRobot().interact() SyncSignInRobot().interact()
return SyncSignInRobot.Transition() return SyncSignInRobot.Transition()
@ -222,7 +223,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton.click() bookmarksButton().click()
assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list")) assertUIObjectExists(itemWithResId("$packageName:id/bookmark_list"))
BookmarksRobot().interact() BookmarksRobot().interact()
@ -230,7 +231,7 @@ class ThreeDotMenuMainRobot {
} }
fun clickNewTabButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun clickNewTabButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
normalBrowsingNewTabButton.click() normalBrowsingNewTabButton().click()
SearchRobot().interact() SearchRobot().interact()
return SearchRobot.Transition() return SearchRobot.Transition()
@ -239,7 +240,7 @@ class ThreeDotMenuMainRobot {
fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition { fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
threeDotMenuRecyclerView().perform(swipeDown()) threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
historyButton.click() historyButton().click()
HistoryRobot().interact() HistoryRobot().interact()
return HistoryRobot.Transition() return HistoryRobot.Transition()
@ -247,7 +248,7 @@ class ThreeDotMenuMainRobot {
fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
addBookmarkButton.click() addBookmarkButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -263,7 +264,7 @@ class ThreeDotMenuMainRobot {
fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openHelp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Help")), waitingTime)
helpButton.click() helpButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -278,7 +279,7 @@ class ThreeDotMenuMainRobot {
waitingTime, waitingTime,
) )
customizeHomeButton.click() customizeHomeButton().click()
mDevice.findObject( mDevice.findObject(
UiSelector().resourceId("$packageName:id/recycler_view"), UiSelector().resourceId("$packageName:id/recycler_view"),
@ -289,21 +290,21 @@ class ThreeDotMenuMainRobot {
} }
fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
forwardButton.click() forwardButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goToPreviousPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
backButton.click() backButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition { fun clickShareButton(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
shareButton.click() shareButton().click()
Log.i(TAG, "clickShareButton: Clicked main menu share button") Log.i(TAG, "clickShareButton: Clicked main menu share button")
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
@ -320,7 +321,7 @@ class ThreeDotMenuMainRobot {
} }
fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
refreshButton.also { refreshButton().also {
Log.i(TAG, "refreshPage: Looking for refresh button") Log.i(TAG, "refreshPage: Looking for refresh button")
it.waitForExists(waitingTime) it.waitForExists(waitingTime)
it.click() it.click()
@ -349,7 +350,7 @@ class ThreeDotMenuMainRobot {
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
reportSiteIssueButton.click() reportSiteIssueButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -359,7 +360,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
findInPageButton.click() findInPageButton().click()
FindInPageRobot().interact() FindInPageRobot().interact()
return FindInPageRobot.Transition() return FindInPageRobot.Transition()
@ -367,7 +368,7 @@ class ThreeDotMenuMainRobot {
fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun openWhatsNew(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Whats new")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Whats new")), waitingTime)
whatsNewButton.click() whatsNewButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -385,7 +386,7 @@ class ThreeDotMenuMainRobot {
fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
for (i in 1..RETRY_COUNT) { for (i in 1..RETRY_COUNT) {
try { try {
addToShortcutsButton.also { addToShortcutsButton().also {
it.waitForExists(waitingTime) it.waitForExists(waitingTime)
it.click() it.click()
} }
@ -416,7 +417,7 @@ class ThreeDotMenuMainRobot {
} }
fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { fun openAddToHomeScreen(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
addToHomeScreenButton.clickAndWaitForNewWindow(waitingTime) addToHomeScreenButton().clickAndWaitForNewWindow(waitingTime)
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
return AddToHomeScreenRobot.Transition() return AddToHomeScreenRobot.Transition()
@ -437,7 +438,7 @@ class ThreeDotMenuMainRobot {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveToCollectionButton.click() saveToCollectionButton().click()
CollectionRobot().interact() CollectionRobot().interact()
return CollectionRobot.Transition() return CollectionRobot.Transition()
} }
@ -465,7 +466,7 @@ class ThreeDotMenuMainRobot {
fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton.click() desktopSiteButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -481,8 +482,8 @@ class ThreeDotMenuMainRobot {
fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun clickPrintButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp()) threeDotMenuRecyclerView().perform(swipeUp())
printButton.waitForExists(waitingTime) printButton().waitForExists(waitingTime)
printButton.click() printButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -558,7 +559,7 @@ private fun openInAppButton() =
private fun clickAddonsManagerButton() { private fun clickAddonsManagerButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton.click() addOnsButton().click()
} }
private fun shareAllTabsButton() = private fun shareAllTabsButton() =
@ -571,15 +572,15 @@ private fun assertShareAllTabsButton() {
) )
} }
private val bookmarksButton = private fun bookmarksButton() =
itemContainingText(getStringResource(R.string.library_bookmarks)) itemContainingText(getStringResource(R.string.library_bookmarks))
private val historyButton = private fun historyButton() =
itemContainingText(getStringResource(R.string.library_history)) itemContainingText(getStringResource(R.string.library_history))
private val downloadsButton = private fun downloadsButton() =
itemContainingText(getStringResource(R.string.library_downloads)) itemContainingText(getStringResource(R.string.library_downloads))
private val addOnsButton = private fun addOnsButton() =
itemContainingText(getStringResource(R.string.browser_menu_add_ons)) itemContainingText(getStringResource(R.string.browser_menu_add_ons))
private val desktopSiteButton = private fun desktopSiteButton() =
itemContainingText(getStringResource(R.string.browser_menu_desktop_site)) itemContainingText(getStringResource(R.string.browser_menu_desktop_site))
private fun desktopSiteToggle(state: Boolean) = private fun desktopSiteToggle(state: Boolean) =
checkedItemWithResIdAndText( checkedItemWithResIdAndText(
@ -587,31 +588,32 @@ private fun desktopSiteToggle(state: Boolean) =
getStringResource(R.string.browser_menu_desktop_site), getStringResource(R.string.browser_menu_desktop_site),
state, state,
) )
private val whatsNewButton = private fun whatsNewButton() =
itemContainingText(getStringResource(R.string.browser_menu_whats_new)) itemContainingText(getStringResource(R.string.browser_menu_whats_new))
private val helpButton = private fun helpButton() =
itemContainingText(getStringResource(R.string.browser_menu_help)) itemContainingText(getStringResource(R.string.browser_menu_help))
private val customizeHomeButton = private fun customizeHomeButton() =
itemContainingText(getStringResource(R.string.browser_menu_customize_home_1)) itemContainingText(getStringResource(R.string.browser_menu_customize_home_1))
private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) = private fun settingsButton(localizedText: String = getStringResource(R.string.browser_menu_settings)) =
itemContainingText(localizedText) itemContainingText(localizedText)
private val syncAndSaveDataButton = private fun syncAndSaveDataButton() =
itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data)) itemContainingText(getStringResource(R.string.sync_menu_sync_and_save_data))
private val normalBrowsingNewTabButton = private fun normalBrowsingNewTabButton() =
itemContainingText(getStringResource(R.string.library_new_tab)) itemContainingText(getStringResource(R.string.library_new_tab))
private val addBookmarkButton = private fun addBookmarkButton() =
itemWithResIdAndText( itemWithResIdAndText(
"$packageName:id/checkbox", "$packageName:id/checkbox",
getStringResource(R.string.browser_menu_add), getStringResource(R.string.browser_menu_add),
) )
private val findInPageButton = itemContainingText(getStringResource(R.string.browser_menu_find_in_page)) private fun findInPageButton() = itemContainingText(getStringResource(R.string.browser_menu_find_in_page))
private val reportSiteIssueButton = itemContainingText("Report Site Issue") private fun translateButton() = itemContainingText(getStringResource(R.string.browser_menu_translations))
private val addToHomeScreenButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen)) private fun reportSiteIssueButton() = itemContainingText("Report Site Issue")
private val addToShortcutsButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts)) private fun addToHomeScreenButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
private val saveToCollectionButton = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2)) private fun addToShortcutsButton() = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts))
private val printContentButton = itemContainingText(getStringResource(R.string.menu_print)) private fun saveToCollectionButton() = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2))
private val backButton = itemWithDescription(getStringResource(R.string.browser_menu_back)) private fun printContentButton() = itemContainingText(getStringResource(R.string.menu_print))
private val forwardButton = itemWithDescription(getStringResource(R.string.browser_menu_forward)) private fun backButton() = itemWithDescription(getStringResource(R.string.browser_menu_back))
private val shareButton = itemWithDescription(getStringResource(R.string.share_button_content_description)) private fun forwardButton() = itemWithDescription(getStringResource(R.string.browser_menu_forward))
private val refreshButton = itemWithDescription(getStringResource(R.string.browser_menu_refresh)) private fun shareButton() = itemWithDescription(getStringResource(R.string.share_button_content_description))
private val printButton = itemWithText("Print") private fun refreshButton() = itemWithDescription(getStringResource(R.string.browser_menu_refresh))
private fun printButton() = itemWithText("Print")

@ -354,6 +354,7 @@
<service <service
android:name=".downloads.DownloadService" android:name=".downloads.DownloadService"
android:foregroundServiceType="dataSync"
android:exported="false" /> android:exported="false" />
<receiver <receiver
@ -374,8 +375,14 @@
</intent-filter> </intent-filter>
</receiver> </receiver>
<service android:name=".session.PrivateNotificationService" <service
android:exported="false" /> android:name=".session.PrivateNotificationService"
android:exported="false"
android:foregroundServiceType="specialUse">
<property
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
android:value="This foreground service allows users to easily remove private tabs from the notification" />
</service>
<service <service
android:name=".messaging.NotificationDismissedService" android:name=".messaging.NotificationDismissedService"

@ -39,4 +39,5 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromRecentlyClosed(R.id.recentlyClosedFragment), FromRecentlyClosed(R.id.recentlyClosedFragment),
FromReviewQualityCheck(R.id.reviewQualityCheckFragment), FromReviewQualityCheck(R.id.reviewQualityCheckFragment),
FromAddonsManagementFragment(R.id.addonsManagementFragment), FromAddonsManagementFragment(R.id.addonsManagementFragment),
FromTranslationsDialogFragment(R.id.translationsDialogFragment),
} }

@ -74,6 +74,11 @@ object FeatureFlags {
*/ */
const val fxSuggest = true const val fxSuggest = true
/**
* Allows users to enable SuggestStrongPassword feature.
*/
const val suggestStrongPassword = true
/** /**
* Enable Meta attribution. * Enable Meta attribution.
*/ */

@ -23,7 +23,7 @@ class FenixLogSink(
priority: Log.Priority, priority: Log.Priority,
tag: String?, tag: String?,
throwable: Throwable?, throwable: Throwable?,
message: String?, message: String,
) { ) {
if (priority == Log.Priority.DEBUG && !logsDebug) { if (priority == Log.Priority.DEBUG && !logsDebug) {
return return

@ -29,14 +29,12 @@ import androidx.annotation.CallSuper
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
@ -89,10 +87,8 @@ import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.SplashScreen import org.mozilla.fenix.GleanMetrics.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.addons.ExtensionsProcessDisabledController
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
@ -103,18 +99,20 @@ import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.databinding.ActivityHomeBinding import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.debugsettings.ui.DebugOverlay import org.mozilla.fenix.debugsettings.ui.FenixOverlay
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getBreadcrumbMessage
import org.mozilla.fenix.ext.getIntentSessionId
import org.mozilla.fenix.ext.getIntentSource
import org.mozilla.fenix.ext.getNavDirections
import org.mozilla.fenix.ext.hasTopDestination import org.mozilla.fenix.ext.hasTopDestination
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.setNavigationIcon import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.extension.WebExtensionPromptFeature import org.mozilla.fenix.extension.WebExtensionPromptFeature
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.AssistIntentProcessor import org.mozilla.fenix.home.intent.AssistIntentProcessor
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor import org.mozilla.fenix.home.intent.HomeDeepLinkIntentProcessor
@ -124,11 +122,7 @@ import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor import org.mozilla.fenix.home.intent.ReEngagementIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.messaging.FenixMessageSurfaceId import org.mozilla.fenix.messaging.FenixMessageSurfaceId
import org.mozilla.fenix.messaging.FenixNimbusMessagingController import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessageNotificationWorker import org.mozilla.fenix.messaging.MessageNotificationWorker
@ -143,29 +137,12 @@ import org.mozilla.fenix.perf.ProfilerMarkers
import org.mozilla.fenix.perf.StartupPathProvider import org.mozilla.fenix.perf.StartupPathProvider
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.shopping.ReviewQualityCheckFragmentDirections
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor.Companion.ACTION_OPEN_PRIVATE_TAB
import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment import org.mozilla.fenix.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.Locale import java.util.Locale
@ -176,7 +153,7 @@ import java.util.Locale
* - home screen * - home screen
* - browser screen * - browser screen
*/ */
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList", "LongMethod") @SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL // DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// we need to store startup timestamp for warm startup. we cant directly store // we need to store startup timestamp for warm startup. we cant directly store
@ -207,8 +184,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
) )
} }
private val extensionsProcessDisabledPromptObserver by lazy { private val extensionsProcessDisabledForegroundController by lazy {
ExtensionsProcessDisabledController(this@HomeActivity) ExtensionsProcessDisabledForegroundController(this@HomeActivity)
}
private val extensionsProcessDisabledBackgroundController by lazy {
ExtensionsProcessDisabledBackgroundController(
browserStore = components.core.store,
appStore = components.appStore,
)
} }
private val serviceWorkerSupport by lazy { private val serviceWorkerSupport by lazy {
@ -300,9 +284,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
visibility = View.VISIBLE visibility = View.VISIBLE
setContent { setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { FenixOverlay(
DebugOverlay() browserStore = components.core.store,
} inactiveTabsEnabled = settings().inactiveTabsAreEnabled,
)
} }
} else { } else {
setContent {} setContent {}
@ -348,7 +333,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Unless the activity is recreated, navigate to home first (without rendering it) // Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack. // to add it to the back stack.
if (savedInstanceState == null) { if (savedInstanceState == null) {
navigateToHome() navigateToHome(navHost.navController)
} }
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) { if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
@ -392,7 +377,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
lifecycle.addObservers( lifecycle.addObservers(
webExtensionPopupObserver, webExtensionPopupObserver,
extensionsProcessDisabledPromptObserver, extensionsProcessDisabledForegroundController,
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport, serviceWorkerSupport,
webExtensionPromptFeature, webExtensionPromptFeature,
) )
@ -699,7 +685,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
startupPathProvider.onIntentReceived(intent) startupPathProvider.onIntentReceived(intent)
} }
open fun handleNewIntent(intent: Intent) { @VisibleForTesting
internal fun handleNewIntent(intent: Intent) {
if (this is ExternalAppBrowserActivity) {
return
}
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960 // https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb( breadcrumb(
@ -886,20 +877,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onUserLeaveHint() super.onUserLeaveHint()
} }
protected open fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: false"
}
@VisibleForTesting(otherwise = PROTECTED)
internal open fun getIntentSource(intent: SafeIntent): String? {
return when {
intent.isLauncherIntent -> APP_ICON
intent.action == Intent.ACTION_VIEW -> "LINK"
else -> null
}
}
/** /**
* External sources such as 3rd party links and shortcuts use this function to enter * External sources such as 3rd party links and shortcuts use this function to enter
* private mode directly before the content view is created. Returns the mode set by the intent * private mode directly before the content view is created. Returns the mode set by the intent
@ -984,8 +961,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
protected open fun getIntentSessionId(intent: SafeIntent): String? = null
/** /**
* Navigates to the browser fragment and loads a URL or performs a search (depending on the * Navigates to the browser fragment and loads a URL or performs a search (depending on the
* value of [searchTermOrURL]). * value of [searchTermOrURL]).
@ -1003,7 +978,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* was opened from history. * was opened from history.
* @param additionalHeaders The extra headers to use when loading the URL. * @param additionalHeaders The extra headers to use when loading the URL.
*/ */
@Suppress("LongParameterList")
fun openToBrowserAndLoad( fun openToBrowserAndLoad(
searchTermOrURL: String, searchTermOrURL: String,
newTab: Boolean, newTab: Boolean,
@ -1038,65 +1012,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
protected open fun getNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? = when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHome ->
HomeFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromWallpaper ->
WallpaperSettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchDialog ->
SearchDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSettings ->
SettingsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromBookmarks ->
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistoryMetadataGroup ->
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHttpsOnlyMode ->
HttpsOnlyFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtection ->
TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionDialog ->
TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSavedLoginsFragment ->
SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddNewDeviceFragment ->
AddNewDeviceFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSearchEngineFragment ->
SearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSaveSearchEngineFragment ->
SaveSearchEngineFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonDetailsFragment ->
AddonDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddonPermissionsDetailsFragment ->
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabsTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
BrowserDirection.FromReviewQualityCheck -> ReviewQualityCheckFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
BrowserDirection.FromAddonsManagementFragment -> AddonsManagementFragmentDirections.actionGlobalBrowser(
customTabSessionId,
)
}
/** /**
* Loads a URL or performs a search (depending on the value of [searchTermOrURL]). * Loads a URL or performs a search (depending on the value of [searchTermOrURL]).
* *
@ -1194,7 +1109,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
settings().openNextTabInDesktopMode = false settings().openNextTabInDesktopMode = false
} }
open fun navigateToBrowserOnColdStart() { @VisibleForTesting
internal fun navigateToBrowserOnColdStart() {
if (this is ExternalAppBrowserActivity) {
return
}
// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) { if (settings().shouldReturnToBrowser && !browsingModeManager.mode.isPrivate) {
@ -1203,8 +1123,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
open fun navigateToHome() { @VisibleForTesting
navHost.navController.navigate(NavGraphDirections.actionStartupHome()) internal fun navigateToHome(navController: NavController) {
if (this is ExternalAppBrowserActivity) {
return
}
navController.navigate(NavGraphDirections.actionStartupHome())
} }
override fun attachBaseContext(base: Context) { override fun attachBaseContext(base: Context) {

@ -0,0 +1,49 @@
/* 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.addons
import android.os.Handler
import android.os.Looper
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptObserver
import org.mozilla.fenix.components.AppStore
import kotlin.system.exitProcess
/**
* Controller for handling extensions process spawning disabled events. This is for when the app is
* in background, the app is killed to prevent extensions from being disabled and network requests
* continuing.
*
* @param browserStore The [BrowserStore] which holds the state for showing the dialog.
* @param appStore The [AppStore] containing the application state.
* @param onExtensionsProcessDisabled Invoked when the app is in background and extensions process
* is disabled.
*/
class ExtensionsProcessDisabledBackgroundController(
browserStore: BrowserStore,
appStore: AppStore,
onExtensionsProcessDisabled: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver(
store = browserStore,
shouldCancelOnStop = false,
onShowExtensionsProcessDisabledPrompt = {
if (!appStore.state.isForeground) {
onExtensionsProcessDisabled()
}
},
) {
companion object {
/**
* When a dialog can't be shown because the app is in the background, instead the app will
* be killed to prevent leaking network data without extensions enabled.
*/
private fun killApp() {
Handler(Looper.getMainLooper()).post {
exitProcess(0)
}
}
}
}

@ -5,8 +5,6 @@
package org.mozilla.fenix.addons package org.mozilla.fenix.addons
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
@ -20,35 +18,30 @@ import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptO
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import kotlin.system.exitProcess
/** /**
* Controller for handling extensions process spawning disabled events. When the app is in * Controller for handling extensions process spawning disabled events. When the app is in
* foreground this will call for a dialog to decide on correct action to take (retry enabling * foreground this will call for a dialog to decide on correct action to take (retry enabling
* process spawning or disable extensions). When in background, we kill the app to prevent * process spawning or disable extensions).
* extensions from being disabled and network requests continuing.
* *
* @param context to show the AlertDialog * @param context to show the AlertDialog
* @param browserStore The [BrowserStore] which holds the state for showing the dialog * @param browserStore The [BrowserStore] which holds the state for showing the dialog
* @param appStore The [AppStore] containing the application state * @param appStore The [AppStore] containing the application state
* @param builder to use for creating the dialog which can be styled as needed * @param builder to use for creating the dialog which can be styled as needed
* @param appName to be added to the message. Optional and mainly relevant for testing * @param appName to be added to the message. Optional and mainly relevant for testing
* @param onKillApp called when the app is backgrounded and extensions process is disabled
*/ */
class ExtensionsProcessDisabledController( class ExtensionsProcessDisabledForegroundController(
@UiContext context: Context, @UiContext context: Context,
browserStore: BrowserStore = context.components.core.store, browserStore: BrowserStore = context.components.core.store,
appStore: AppStore = context.components.appStore, appStore: AppStore = context.components.appStore,
builder: AlertDialog.Builder = AlertDialog.Builder(context), builder: AlertDialog.Builder = AlertDialog.Builder(context),
appName: String = context.appName, appName: String = context.appName,
onKillApp: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver( ) : ExtensionsProcessDisabledPromptObserver(
browserStore, store = browserStore,
shouldCancelOnStop = true,
{ {
if (appStore.state.isForeground) { if (appStore.state.isForeground) {
presentDialog(context, browserStore, builder, appName) presentDialog(context, browserStore, builder, appName)
} else {
onKillApp.invoke()
} }
}, },
) { ) {
@ -61,16 +54,6 @@ class ExtensionsProcessDisabledController(
companion object { companion object {
private var shouldCreateDialog: Boolean = true private var shouldCreateDialog: Boolean = true
/**
* When a dialog can't be shown because the app is in the background, instead the app will
* be killed to prevent leaking network data without extensions enabled.
*/
private fun killApp() {
Handler(Looper.getMainLooper()).post {
exitProcess(0)
}
}
/** /**
* Present a dialog to the user notifying of extensions process spawning disabled and also asking * Present a dialog to the user notifying of extensions process spawning disabled and also asking
* whether they would like to continue trying or disable extensions. If the user chooses to retry, * whether they would like to continue trying or disable extensions. If the user chooses to retry,

@ -12,6 +12,7 @@ import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -31,9 +32,11 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@ -59,6 +62,7 @@ import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.feature.accounts.FxaCapability import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature import mozilla.components.feature.accounts.FxaWebChannelFeature
import mozilla.components.feature.app.links.AppLinksFeature import mozilla.components.feature.app.links.AppLinksFeature
@ -79,6 +83,7 @@ import mozilla.components.feature.prompts.dialog.FullScreenNotificationDialog
import mozilla.components.feature.prompts.identitycredential.DialogColors import mozilla.components.feature.prompts.identitycredential.DialogColors
import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider import mozilla.components.feature.prompts.identitycredential.DialogColorsProvider
import mozilla.components.feature.prompts.login.LoginDelegate import mozilla.components.feature.prompts.login.LoginDelegate
import mozilla.components.feature.prompts.login.SuggestStrongPasswordDelegate
import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.prompts.share.ShareDelegate
import mozilla.components.feature.readerview.ReaderViewFeature import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.feature.search.SearchFeature import mozilla.components.feature.search.SearchFeature
@ -95,6 +100,8 @@ import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.glean.private.NoExtras import mozilla.components.service.glean.private.NoExtras
import mozilla.components.service.sync.autofill.DefaultCreditCardValidationDelegate import mozilla.components.service.sync.autofill.DefaultCreditCardValidationDelegate
import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
import mozilla.components.service.sync.logins.LoginsApiException
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
@ -108,6 +115,7 @@ import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.MediaState import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -160,6 +168,7 @@ import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.coroutines.cancellation.CancellationException
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/** /**
@ -460,6 +469,7 @@ abstract class BaseBrowserFragment :
browserToolbarView.view.display.setOnSiteSecurityClickedListener { browserToolbarView.view.display.setOnSiteSecurityClickedListener {
showQuickSettingsDialog() showQuickSettingsDialog()
Events.browserToolbarSecurityIndicatorTapped.record()
} }
contextMenuFeature.set( contextMenuFeature.set(
@ -731,6 +741,18 @@ abstract class BaseBrowserFragment :
} }
} }
}, },
suggestStrongPasswordDelegate = object : SuggestStrongPasswordDelegate {
override val strongPasswordPromptViewListenerView
get() = binding.suggestStrongPasswordBar
},
isSuggestStrongPasswordEnabled = context.settings().enableSuggestStrongPassword,
onSaveLoginWithStrongPassword = { url, password ->
handleOnSaveLoginWithGeneratedStrongPassword(
passwordsStorage = context.components.core.passwordsStorage,
url = url,
password = password,
)
},
creditCardDelegate = object : CreditCardDelegate { creditCardDelegate = object : CreditCardDelegate {
override val creditCardPickerView override val creditCardPickerView
get() = binding.creditCardSelectBar get() = binding.creditCardSelectBar
@ -894,9 +916,12 @@ abstract class BaseBrowserFragment :
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false) binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
if (binding.swipeRefresh.isEnabled) { if (binding.swipeRefresh.isEnabled) {
val primaryTextColor = val primaryTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
ThemeManager.resolveAttribute(R.attr.textPrimary, context) val primaryBackgroundColor = ThemeManager.resolveAttribute(R.attr.layer2, context)
binding.swipeRefresh.setColorSchemeColors(primaryTextColor) binding.swipeRefresh.apply {
setColorSchemeResources(primaryTextColor)
setProgressBackgroundColorSchemeResource(primaryBackgroundColor)
}
swipeRefreshFeature.set( swipeRefreshFeature.set(
feature = SwipeRefreshFeature( feature = SwipeRefreshFeature(
requireComponents.core.store, requireComponents.core.store,
@ -1651,4 +1676,38 @@ abstract class BaseBrowserFragment :
return isValidStatus && isSameTab return isValidStatus && isSameTab
} }
private fun handleOnSaveLoginWithGeneratedStrongPassword(
passwordsStorage: SyncableLoginsStorage,
url: String,
password: String,
) {
val loginToSave = LoginEntry(
origin = url,
httpRealm = url,
username = "",
password = password,
)
var saveLoginJob: Deferred<Unit>? = null
lifecycleScope.launch(IO) {
saveLoginJob = async {
try {
passwordsStorage.add(loginToSave)
} catch (loginException: LoginsApiException) {
loginException.printStackTrace()
Log.e(
"Add new login",
"Failed to add new login with generated password.",
loginException,
)
}
saveLoginJob?.await()
}
saveLoginJob?.invokeOnCompletion {
if (it is CancellationException) {
saveLoginJob?.cancel()
}
}
}
}
} }

@ -22,7 +22,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
* @param dismissAction Optional callback invoked when the user dismisses the banner. * @param dismissAction Optional callback invoked when the user dismisses the banner.
* @param actionToPerform The action to be performed on action button press. * @param actionToPerform The action to be performed on action button press.
*/ */
@Suppress("LongParameterList")
class DynamicInfoBanner( class DynamicInfoBanner(
private val context: Context, private val context: Context,
container: ViewGroup, container: ViewGroup,

@ -26,7 +26,6 @@ import org.mozilla.fenix.ext.settings
* @property dismissAction Optional callback invoked when the user dismisses the banner. * @property dismissAction Optional callback invoked when the user dismisses the banner.
* @param actionToPerform The action to be performed on action button press. * @param actionToPerform The action to be performed on action button press.
*/ */
@SuppressWarnings("LongParameterList")
open class InfoBanner( open class InfoBanner(
private val context: Context, private val context: Context,
private val container: ViewGroup, private val container: ViewGroup,

@ -53,7 +53,7 @@ class IntentProcessors(
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs.
*/ */
val privateIntentProcessor by lazyMonitored { val privateIntentProcessor by lazyMonitored {
TabIntentProcessor(tabsUseCases, searchUseCases.newTabSearch, isPrivate = true) TabIntentProcessor(tabsUseCases, searchUseCases.newPrivateTabSearch, isPrivate = true)
} }
val customTabIntentProcessor by lazyMonitored { val customTabIntentProcessor by lazyMonitored {

@ -223,7 +223,9 @@ class DefaultBrowserToolbarController(
override fun handleTranslationsButtonClick() { override fun handleTranslationsButtonClick() {
val directions = val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment() BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
sessionId = currentSession?.id,
)
navController.navigateSafe(R.id.browserFragment, directions) navController.navigateSafe(R.id.browserFragment, directions)
} }

@ -406,6 +406,14 @@ class DefaultBrowserToolbarMenuController(
.show() .show()
} }
} }
ToolbarMenu.Item.Translate -> {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTranslationsDialogFragment(
sessionId = currentSession?.id,
)
navController.navigateSafe(R.id.browserFragment, directions)
}
} }
} }
@ -420,7 +428,7 @@ class DefaultBrowserToolbarMenuController(
} }
} }
@Suppress("ComplexMethod") @Suppress("ComplexMethod", "LongMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
when (item) { when (item) {
is ToolbarMenu.Item.OpenInFenix -> is ToolbarMenu.Item.OpenInFenix ->
@ -433,10 +441,19 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app"))
is ToolbarMenu.Item.CustomizeReaderView -> is ToolbarMenu.Item.CustomizeReaderView ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance"))
is ToolbarMenu.Item.Back -> is ToolbarMenu.Item.Back -> {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back")) if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back"))
}
}
is ToolbarMenu.Item.Forward -> is ToolbarMenu.Item.Forward ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward")) if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("forward"))
}
is ToolbarMenu.Item.Reload -> is ToolbarMenu.Item.Reload ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload"))
is ToolbarMenu.Item.Stop -> is ToolbarMenu.Item.Stop ->
@ -483,6 +500,12 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser"))
is ToolbarMenu.Item.RemoveFromTopSites -> is ToolbarMenu.Item.RemoveFromTopSites ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("remove_from_top_sites")) Events.browserMenuAction.record(Events.BrowserMenuActionExtra("remove_from_top_sites"))
ToolbarMenu.Item.Translate -> Events.browserMenuAction.record(
Events.BrowserMenuActionExtra(
"translate",
),
)
} }
} }

@ -42,7 +42,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.Settings
/** /**
* Builds the toolbar object used with the 3-dot menu in the browser fragment. * Builds the toolbar object used with the 3-dot menu in the browser fragment.
@ -55,7 +54,7 @@ import org.mozilla.fenix.utils.Settings
* @param pinnedSiteStorage Used to check if the current url is a pinned site. * @param pinnedSiteStorage Used to check if the current url is a pinned site.
* @property isPinningSupported true if the launcher supports adding shortcuts. * @property isPinningSupported true if the launcher supports adding shortcuts.
*/ */
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @Suppress("LargeClass", "TooManyFunctions")
open class DefaultToolbarMenu( open class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val store: BrowserStore, private val store: BrowserStore,
@ -193,6 +192,14 @@ open class DefaultToolbarMenu(
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let { fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active store.state.findTab(it.id)?.readerState?.active
} ?: false } ?: false
/**
* Should Translations menu item be visible?
*/
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowTranslations(): Boolean = selectedSession?.let {
context.settings().enableTranslations
} ?: false
// End of predicates // // End of predicates //
private val installToHomescreen = BrowserMenuHighlightableItem( private val installToHomescreen = BrowserMenuHighlightableItem(
@ -248,6 +255,14 @@ open class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.FindInPage) onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
} }
private val translationsItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_translations),
imageResource = R.drawable.mozac_ic_translate_24,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.Translate)
}
private val desktopSiteItem = BrowserMenuImageSwitch( private val desktopSiteItem = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop, imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site), label = context.getString(R.string.browser_menu_desktop_site),
@ -405,6 +420,7 @@ open class DefaultToolbarMenu(
syncMenuItem(), syncMenuItem(),
BrowserMenuDivider(), BrowserMenuDivider(),
findInPageItem, findInPageItem,
translationsItem.apply { visible = ::shouldShowTranslations },
desktopSiteItem, desktopSiteItem,
openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab }, openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab },
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },

@ -76,7 +76,6 @@ abstract class ToolbarIntegration(
} }
} }
@Suppress("LongParameterList")
class DefaultToolbarIntegration( class DefaultToolbarIntegration(
context: Context, context: Context,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,

@ -18,6 +18,11 @@ interface ToolbarMenu {
*/ */
object OpenInRegularTab : Item() object OpenInRegularTab : Item()
object FindInPage : Item() object FindInPage : Item()
/**
* Opens the translations flow.
*/
object Translate : Item()
object Share : Item() object Share : Item()
data class Back(val viewHistory: Boolean) : Item() data class Back(val viewHistory: Boolean) : Item()
data class Forward(val viewHistory: Boolean) : Item() data class Forward(val viewHistory: Boolean) : Item()

@ -41,7 +41,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* By default set to a solid color in [DefaultImagePlaceholder]. * By default set to a solid color in [DefaultImagePlaceholder].
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun Image( fun Image(
url: String, url: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

@ -7,10 +7,22 @@ package org.mozilla.fenix.compose
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -20,6 +32,9 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
/** /**
@ -67,6 +82,12 @@ fun LinkText(
linkTextDecoration, linkTextDecoration,
) )
val showDialog = remember { mutableStateOf(false) }
val linksAvailable = stringResource(id = R.string.a11y_links_available)
if (showDialog.value) {
LinksDialog(linkTextStates) { showDialog.value = false }
}
// When using UrlAnnotation, talkback shows links in a separate dialog and // When using UrlAnnotation, talkback shows links in a separate dialog and
// opens them in the default browser. Since this component allows the caller to define the // opens them in the default browser. Since this component allows the caller to define the
// onClick behaviour - e.g. to open the link in in-app custom tab, here StringAnnotation is used // onClick behaviour - e.g. to open the link in in-app custom tab, here StringAnnotation is used
@ -76,12 +97,17 @@ fun LinkText(
style = style, style = style,
modifier = Modifier.semantics(mergeDescendants = true) { modifier = Modifier.semantics(mergeDescendants = true) {
onClick { onClick {
linkTextStates.firstOrNull()?.let { if (linkTextStates.size > 1) {
it.onClick(it.url) showDialog.value = true
} else {
linkTextStates.firstOrNull()?.let {
it.onClick(it.url)
}
} }
return@onClick true return@onClick true
} }
contentDescription = "$annotatedString $linksAvailable"
}, },
onClick = { charOffset -> onClick = { charOffset ->
onTextClick(annotatedString, charOffset, linkTextStates) onTextClick(annotatedString, charOffset, linkTextStates)
@ -89,6 +115,60 @@ fun LinkText(
) )
} }
@Composable
private fun LinksDialog(
linkTextStates: List<LinkTextState>,
onDismissRequest: () -> Unit,
) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(
modifier = Modifier
.fillMaxWidth(),
shape = RoundedCornerShape(8.dp),
) {
Column(
modifier = Modifier
.background(color = FirefoxTheme.colors.layer2)
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(id = R.string.a11y_links_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
linkTextStates.forEach { linkText ->
TextButton(
onClick = { linkText.onClick(linkText.url) },
modifier = Modifier
.align(Alignment.Start),
) {
Text(
text = linkText.text,
color = FirefoxTheme.colors.textAccent,
textDecoration = TextDecoration.Underline,
style = FirefoxTheme.typography.button,
)
}
}
TextButton(
onClick = { onDismissRequest() },
modifier = Modifier
.align(Alignment.End),
) {
Text(
text = stringResource(id = R.string.standard_snackbar_error_dismiss),
color = FirefoxTheme.colors.textAccent,
style = FirefoxTheme.typography.button,
)
}
}
}
}
}
@VisibleForTesting @VisibleForTesting
internal fun onTextClick( internal fun onTextClick(
annotatedString: AnnotatedString, annotatedString: AnnotatedString,
@ -231,3 +311,27 @@ private fun MultipleLinksPreview() {
} }
} }
} }
@Preview
@Composable
private fun LinksDialogPreview() {
val state1 = LinkTextState(
text = "clickable text",
url = "www.mozilla.com",
onClick = {},
)
val state2 = LinkTextState(
text = "another clickable text",
url = "www.mozilla.com",
onClick = {},
)
val linkTextStateList = listOf(state1, state2)
FirefoxTheme {
LinksDialog(
linkTextStates = linkTextStateList,
onDismissRequest = {},
)
}
}

@ -35,6 +35,7 @@ private const val DISABLED_ALPHA = 0.5f
* UI for a switch with label that can be on or off. * UI for a switch with label that can be on or off.
* *
* @param label Text to be displayed next to the switch. * @param label Text to be displayed next to the switch.
* @param description An optional description text below the label.
* @param checked Whether or not the switch is checked. * @param checked Whether or not the switch is checked.
* @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked * @param onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested. * state is requested.
@ -44,6 +45,7 @@ private const val DISABLED_ALPHA = 0.5f
@Composable @Composable
fun SwitchWithLabel( fun SwitchWithLabel(
label: String, label: String,
description: String? = null,
checked: Boolean, checked: Boolean,
onCheckedChange: ((Boolean) -> Unit), onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -60,16 +62,28 @@ fun SwitchWithLabel(
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Text( Column(
text = label, modifier = Modifier
color = if (enabled) { .weight(1f),
FirefoxTheme.colors.textPrimary ) {
} else { Text(
FirefoxTheme.colors.textDisabled text = label,
}, color = if (enabled) {
style = FirefoxTheme.typography.subtitle1, FirefoxTheme.colors.textPrimary
modifier = Modifier.weight(1f), } else {
) FirefoxTheme.colors.textDisabled
},
style = FirefoxTheme.typography.subtitle1,
)
description?.let {
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.body2,
)
}
}
Switch( Switch(
modifier = Modifier.clearAndSetSemantics {}, modifier = Modifier.clearAndSetSemantics {},
@ -139,6 +153,7 @@ private fun SwitchWithLabelPreview() {
var enabledSwitchState by remember { mutableStateOf(false) } var enabledSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel( SwitchWithLabel(
label = if (enabledSwitchState) "On" else "Off", label = if (enabledSwitchState) "On" else "Off",
description = "Description text",
checked = enabledSwitchState, checked = enabledSwitchState,
onCheckedChange = { enabledSwitchState = it }, onCheckedChange = { enabledSwitchState = it },
) )

@ -42,7 +42,6 @@ private const val FALLBACK_ICON_SIZE = 36
* @param alignment [Alignment] used to draw the image content. * @param alignment [Alignment] used to draw the image content.
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun TabThumbnail( fun TabThumbnail(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,

@ -38,7 +38,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param fallbackContent The content to display with a thumbnail is unable to be loaded. * @param fallbackContent The content to display with a thumbnail is unable to be loaded.
*/ */
@Composable @Composable
@Suppress("LongParameterList")
fun ThumbnailImage( fun ThumbnailImage(
request: ImageLoadRequest, request: ImageLoadRequest,
storage: ThumbnailStorage, storage: ThumbnailStorage,

@ -38,7 +38,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onClick Optional lambda for handling header clicks. * @param onClick Optional lambda for handling header clicks.
* @param actions Optional Composable for adding UI to the end of the header. * @param actions Optional Composable for adding UI to the end of the header.
*/ */
@Suppress("LongParameterList")
@Composable @Composable
fun ExpandableListHeader( fun ExpandableListHeader(
headerText: String, headerText: String,

@ -91,7 +91,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/ */
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@Composable @Composable
@Suppress("MagicNumber", "LongParameterList", "LongMethod") @Suppress("MagicNumber", "LongMethod")
fun TabGridItem( fun TabGridItem(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,

@ -75,7 +75,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
*/ */
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable @Composable
@Suppress("MagicNumber", "LongMethod", "LongParameterList") @Suppress("MagicNumber", "LongMethod")
fun TabListItem( fun TabListItem(
tab: TabSessionState, tab: TabSessionState,
storage: ThumbnailStorage, storage: ThumbnailStorage,
@ -209,7 +209,6 @@ private fun clickableColor() = when (isSystemInDarkTheme()) {
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun Thumbnail( private fun Thumbnail(
tab: TabSessionState, tab: TabSessionState,
size: Int, size: Int,

@ -5,24 +5,16 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.app.assist.AssistContent import android.app.assist.AssistContent
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import java.security.InvalidParameterException import org.mozilla.fenix.ext.getIntentSessionId
const val EXTRA_IS_SANDBOX_CUSTOM_TAB = "org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB" const val EXTRA_IS_SANDBOX_CUSTOM_TAB = "org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB"
@ -45,52 +37,6 @@ open class ExternalAppBrowserActivity : HomeActivity() {
} }
} }
final override fun getBreadcrumbMessage(destination: NavDestination): String {
val fragmentName = resources.getResourceEntryName(destination.id)
return "Changing to fragment $fragmentName, isCustomTab: true"
}
final override fun getIntentSource(intent: SafeIntent) = "CUSTOM_TAB"
final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId()
override fun navigateToBrowserOnColdStart() {
// No-op for external app
}
override fun navigateToHome() {
// No-op for external app
}
override fun handleNewIntent(intent: Intent) {
// No-op for external app
}
override fun getNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? {
if (customTabSessionId == null) {
finishAndRemoveTask()
return null
}
val manifest = intent
.getWebAppManifest()
?.let { WebAppManifestParser().serialize(it).toString() }
return when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalExternalAppBrowser(
activeSessionId = customTabSessionId,
webAppManifest = manifest,
isSandboxCustomTab = intent.getBooleanExtra(EXTRA_IS_SANDBOX_CUSTOM_TAB, false),
)
else -> throw InvalidParameterException(
"Tried to navigate to ExternalAppBrowserFragment from $from",
)
}
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()

@ -26,7 +26,6 @@ import mozilla.components.feature.pwa.feature.WebAppActivityFeature
import mozilla.components.feature.pwa.feature.WebAppContentFeature import mozilla.components.feature.pwa.feature.WebAppContentFeature
import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature
import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
@ -44,7 +43,7 @@ import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCoo
/** /**
* Fragment used for browsing the web within external apps. * Fragment used for browsing the web within external apps.
*/ */
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler { class ExternalAppBrowserFragment : BaseBrowserFragment() {
private val args by navArgs<ExternalAppBrowserFragmentArgs>() private val args by navArgs<ExternalAppBrowserFragmentArgs>()
@ -212,9 +211,4 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
view, view,
FenixSnackbarDelegate(view), FenixSnackbarDelegate(view),
) )
companion object {
// We only care about millisecond precision for telemetry events
internal const val MS_PRECISION = 1_000_000L
}
} }

@ -0,0 +1,24 @@
/* 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.debugsettings.navigation
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
/**
* A navigation destination for screens within the Debug Drawer.
*
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
* @property onClick Invoked when the destination is clicked to be navigated to.
* @property content The destination's [Composable].
*/
data class DebugDrawerDestination(
val route: String,
@StringRes val title: Int,
val onClick: () -> Unit,
val content: @Composable () -> Unit,
)

@ -0,0 +1,70 @@
/* 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.debugsettings.navigation
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.tabs.TabTools as TabToolsScreen
/**
* The navigation routes for screens within the Debug Drawer.
*
* @property route The unique route used to navigate to the destination. This string can also contain
* optional parameters for arguments or deep linking.
* @property title The string ID of the destination's title.
*/
enum class DebugDrawerRoute(val route: String, @StringRes val title: Int) {
/**
* The navigation route for [TabToolsScreen].
*/
TabTools(
route = "tab_tools",
title = R.string.debug_drawer_tab_tools_title,
),
;
companion object {
/**
* Transforms the values of [DebugDrawerRoute] into a list of [DebugDrawerDestination]s.
*
* @param debugDrawerStore [DebugDrawerStore] used to dispatch navigation actions.
* @param browserStore [BrowserStore] used to add tabs in [TabToolsScreen].
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
fun generateDebugDrawerDestinations(
debugDrawerStore: DebugDrawerStore,
browserStore: BrowserStore,
inactiveTabsEnabled: Boolean,
): List<DebugDrawerDestination> =
DebugDrawerRoute.values().map { debugDrawerRoute ->
val onClick: () -> Unit
val content: @Composable () -> Unit
when (debugDrawerRoute) {
TabTools -> {
onClick = {
debugDrawerStore.dispatch(DebugDrawerAction.NavigateTo.TabTools)
}
content = {
TabToolsScreen(
store = browserStore,
inactiveTabsEnabled = inactiveTabsEnabled,
)
}
}
}
DebugDrawerDestination(
route = debugDrawerRoute.route,
title = debugDrawerRoute.title,
onClick = onClick,
content = content,
)
}
}
}

@ -0,0 +1,46 @@
/* 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.debugsettings.store
import mozilla.components.lib.state.Action
import org.mozilla.fenix.debugsettings.ui.DebugDrawerHome
import org.mozilla.fenix.debugsettings.tabs.TabTools as TabToolsScreen
/**
* [Action] implementation related to [DebugDrawerStore].
*/
sealed class DebugDrawerAction : Action {
/**
* [DebugDrawerAction] fired when the user opens the drawer.
*/
object DrawerOpened : DebugDrawerAction()
/**
* [DebugDrawerAction] fired when the user closes the drawer.
*/
object DrawerClosed : DebugDrawerAction()
/**
* [DebugDrawerAction] fired when a navigation event occurs for a specific destination.
*/
sealed class NavigateTo : DebugDrawerAction() {
/**
* [NavigateTo] action fired when the debug drawer needs to navigate to [DebugDrawerHome].
*/
object Home : NavigateTo()
/**
* [NavigateTo] action fired when the debug drawer needs to navigate to [TabToolsScreen].
*/
object TabTools : NavigateTo()
}
/**
* [DebugDrawerAction] fired when a back navigation event occurs.
*/
object OnBackPressed : DebugDrawerAction()
}

@ -0,0 +1,45 @@
/* 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.debugsettings.store
import androidx.navigation.NavHostController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.ui.DEBUG_DRAWER_HOME_ROUTE
/**
* Middleware that handles navigation events for the Debug Drawer feature.
*
* @param navController [NavHostController] used to execute any navigation actions on the UI.
* @param scope [CoroutineScope] used to make calls to the main thread.
*/
class DebugDrawerNavigationMiddleware(
private val navController: NavHostController,
private val scope: CoroutineScope,
) : Middleware<DebugDrawerState, DebugDrawerAction> {
override fun invoke(
context: MiddlewareContext<DebugDrawerState, DebugDrawerAction>,
next: (DebugDrawerAction) -> Unit,
action: DebugDrawerAction,
) {
next(action)
scope.launch {
when (action) {
is DebugDrawerAction.NavigateTo.Home -> navController.popBackStack(
route = DEBUG_DRAWER_HOME_ROUTE,
inclusive = false,
)
is DebugDrawerAction.NavigateTo.TabTools ->
navController.navigate(route = DebugDrawerRoute.TabTools.route)
is DebugDrawerAction.OnBackPressed -> navController.popBackStack()
is DebugDrawerAction.DrawerOpened, DebugDrawerAction.DrawerClosed -> Unit // no-op
}
}
}
}

@ -0,0 +1,16 @@
/* 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.debugsettings.store
import mozilla.components.lib.state.State
/**
* UI state of the debug drawer feature.
*
* @property drawerStatus The [DrawerStatus] indicating the physical state of the drawer.
*/
data class DebugDrawerState(
val drawerStatus: DrawerStatus = DrawerStatus.Closed,
) : State

@ -0,0 +1,27 @@
/* 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.debugsettings.store
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
/**
* A [Store] that holds the [DebugDrawerState] for the Debug Drawer and reduces [DebugDrawerAction]s
* dispatched to the store.
*/
class DebugDrawerStore(
initialState: DebugDrawerState = DebugDrawerState(),
middlewares: List<Middleware<DebugDrawerState, DebugDrawerAction>> = emptyList(),
) : Store<DebugDrawerState, DebugDrawerAction>(
initialState,
::reduce,
middlewares,
)
private fun reduce(state: DebugDrawerState, action: DebugDrawerAction): DebugDrawerState = when (action) {
is DebugDrawerAction.DrawerOpened -> state.copy(drawerStatus = DrawerStatus.Open)
is DebugDrawerAction.DrawerClosed -> state.copy(drawerStatus = DrawerStatus.Closed)
is DebugDrawerAction.NavigateTo, DebugDrawerAction.OnBackPressed -> state // handled by [DebugDrawerNavigationMiddleware]
}

@ -0,0 +1,20 @@
/* 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.debugsettings.store
/**
* Possible values of the debug drawer's physical state.
*/
enum class DrawerStatus {
/**
* The state of the drawer when it is closed.
*/
Closed,
/**
* The state of the drawer when it is open.
*/
Open,
}

@ -0,0 +1,328 @@
/* 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.debugsettings.tabs
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.core.text.isDigitsOnly
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.debugsettings.ui.DebugDrawer
import org.mozilla.fenix.ext.maxActiveTime
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Tab Tools UI for [DebugDrawer] that displays the tab counts and allows easy bulk-opening of tabs.
*
* @param store [BrowserStore] used to obtain the tab counts and fire any tab creation actions.
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
@Composable
fun TabTools(
store: BrowserStore,
inactiveTabsEnabled: Boolean,
) {
val tabs by store.observeAsState(initialValue = emptyList()) { state -> state.tabs }
val totalTabCount = remember(tabs) { tabs.size }
val privateTabCount = remember(tabs) { tabs.filter { it.content.private }.size }
val inactiveTabCount = remember(tabs) {
if (inactiveTabsEnabled) {
tabs.filter { it.isNormalTabInactive(maxActiveTime) }.size
} else {
0
}
}
val activeTabCount = remember(tabs) { totalTabCount - privateTabCount - inactiveTabCount }
TabToolsContent(
activeTabCount = activeTabCount,
inactiveTabCount = inactiveTabCount,
privateTabCount = privateTabCount,
totalTabCount = totalTabCount,
inactiveTabsEnabled = inactiveTabsEnabled,
onCreateTabsClick = { quantity, isInactive, isPrivate ->
store.dispatch(
TabListAction.AddMultipleTabsAction(
tabs = generateTabList(
quantity = quantity,
isInactive = isInactive,
isPrivate = isPrivate,
),
),
)
},
)
}
private fun generateTabList(
quantity: Int,
isInactive: Boolean = false,
isPrivate: Boolean = false,
) = List(quantity) {
createTab(
url = "www.example.com",
private = isPrivate,
createdAt = if (isInactive) 0L else System.currentTimeMillis(),
)
}
@Composable
private fun TabToolsContent(
activeTabCount: Int,
inactiveTabCount: Int,
privateTabCount: Int,
totalTabCount: Int,
inactiveTabsEnabled: Boolean,
onCreateTabsClick: ((quantity: Int, isInactive: Boolean, isPrivate: Boolean) -> Unit),
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TabCounter(
activeTabCount = activeTabCount,
inactiveTabCount = inactiveTabCount,
privateTabCount = privateTabCount,
totalTabCount = totalTabCount,
inactiveTabsEnabled = inactiveTabsEnabled,
)
TabCreationTool(
inactiveTabsEnabled = inactiveTabsEnabled,
onCreateTabsClick = onCreateTabsClick,
)
}
}
@Composable
private fun TabCounter(
activeTabCount: Int,
inactiveTabCount: Int,
privateTabCount: Int,
totalTabCount: Int,
inactiveTabsEnabled: Boolean,
) {
Column {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_count_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
Spacer(modifier = Modifier.height(16.dp))
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_normal),
count = activeTabCount.toString(),
)
if (inactiveTabsEnabled) {
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_inactive),
count = inactiveTabCount.toString(),
)
}
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_private),
count = privateTabCount.toString(),
)
Spacer(modifier = Modifier.height(8.dp))
Divider()
Spacer(modifier = Modifier.height(8.dp))
TabCountRow(
tabType = stringResource(R.string.debug_drawer_tab_tools_tab_count_total),
count = totalTabCount.toString(),
)
}
}
@Composable
private fun TabCountRow(
tabType: String,
count: String,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = tabType,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline6,
)
Text(
text = count,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline6,
)
}
}
private const val DEFAULT_TABS_TO_ADD = "1"
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun TabCreationTool(
inactiveTabsEnabled: Boolean,
onCreateTabsClick: ((quantity: Int, isInactive: Boolean, isPrivate: Boolean) -> Unit),
) {
var tabQuantityToCreate by rememberSaveable { mutableStateOf(DEFAULT_TABS_TO_ADD) }
var hasError by rememberSaveable { mutableStateOf(false) }
val keyboardController = LocalSoftwareKeyboardController.current
Column {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_creation_tool_title),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
TextField(
value = tabQuantityToCreate,
onValueChange = {
tabQuantityToCreate = it
hasError = it.isEmpty() || !it.isDigitsOnly() || it.toInt() == 0
},
modifier = Modifier.fillMaxWidth(),
textStyle = FirefoxTheme.typography.subtitle1,
label = {
Text(
text = stringResource(R.string.debug_drawer_tab_tools_tab_creation_tool_text_field_label),
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.caption,
)
},
isError = hasError,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
),
keyboardActions = KeyboardActions(
onDone = {
keyboardController?.hide()
},
),
singleLine = true,
colors = TextFieldDefaults.textFieldColors(
textColor = FirefoxTheme.colors.textPrimary,
backgroundColor = Color.Transparent,
cursorColor = FirefoxTheme.colors.borderFormDefault,
errorCursorColor = FirefoxTheme.colors.borderWarning,
focusedIndicatorColor = FirefoxTheme.colors.borderPrimary,
unfocusedIndicatorColor = FirefoxTheme.colors.borderPrimary,
errorIndicatorColor = FirefoxTheme.colors.borderWarning,
),
)
Spacer(modifier = Modifier.height(8.dp))
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_active),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), false, false)
},
)
Spacer(modifier = Modifier.height(8.dp))
if (inactiveTabsEnabled) {
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_inactive),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), true, false)
},
)
Spacer(modifier = Modifier.height(8.dp))
}
PrimaryButton(
text = stringResource(id = R.string.debug_drawer_tab_tools_tab_creation_tool_button_text_private),
enabled = !hasError,
onClick = {
onCreateTabsClick(tabQuantityToCreate.toInt(), false, true)
},
)
}
}
private data class TabToolsPreviewModel(
val inactiveTabsEnabled: Boolean = true,
)
private class TabToolsPreviewParameterProvider : PreviewParameterProvider<TabToolsPreviewModel> {
override val values: Sequence<TabToolsPreviewModel>
get() = sequenceOf(
TabToolsPreviewModel(
inactiveTabsEnabled = true,
),
TabToolsPreviewModel(
inactiveTabsEnabled = false,
),
)
}
@Composable
@LightDarkPreview
private fun TabToolsPreview(
@PreviewParameter(TabToolsPreviewParameterProvider::class) model: TabToolsPreviewModel,
) {
FirefoxTheme {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer1),
) {
TabTools(
store = BrowserStore(),
inactiveTabsEnabled = model.inactiveTabsEnabled,
)
}
}
}

@ -0,0 +1,138 @@
/* 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.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The debug drawer UI.
*
* @param navController [NavHostController] used to perform navigation actions on the [NavHost].
* @param destinations The list of [DebugDrawerDestination]s (excluding home) used to populate
* the [NavHost] with screens.
* @param onBackButtonClick Invoked when the user taps on the back button in the app bar.
*/
@Composable
fun DebugDrawer(
navController: NavHostController,
destinations: List<DebugDrawerDestination>,
onBackButtonClick: () -> Unit,
) {
var backButtonVisible by remember { mutableStateOf(false) }
var toolbarTitle by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize()) {
TopAppBar(
title = {
Text(
text = toolbarTitle,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
navigationIcon = if (backButtonVisible) {
topBarBackButton(onClick = onBackButtonClick)
} else {
null
},
backgroundColor = FirefoxTheme.colors.layer1,
elevation = 5.dp,
)
NavHost(
navController = navController,
startDestination = DEBUG_DRAWER_HOME_ROUTE,
modifier = Modifier.fillMaxSize(),
) {
composable(route = DEBUG_DRAWER_HOME_ROUTE) {
toolbarTitle = stringResource(id = R.string.debug_drawer_title)
backButtonVisible = false
DebugDrawerHome(destinations = destinations)
}
destinations.forEach { destination ->
composable(route = destination.route) {
toolbarTitle = stringResource(id = destination.title)
backButtonVisible = true
destination.content()
}
}
}
}
}
@Composable
private fun topBarBackButton(onClick: () -> Unit): @Composable () -> Unit = {
IconButton(
onClick = onClick,
) {
Icon(
painter = painterResource(R.drawable.mozac_ic_back_24),
contentDescription = stringResource(R.string.debug_drawer_back_button_content_description),
tint = FirefoxTheme.colors.iconPrimary,
)
}
}
@Composable
@LightDarkPreview
private fun DebugDrawerPreview() {
val navController = rememberNavController()
val destinations = remember {
List(size = 15) { index ->
DebugDrawerDestination(
route = "screen_$index",
title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
}
}
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
DebugDrawer(
navController = navController,
destinations = destinations,
onBackButtonClick = {
navController.popBackStack()
},
)
}
}
}

@ -0,0 +1,144 @@
/* 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.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.appVersionName
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.compose.list.TextListItem
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The navigation route for [DebugDrawerHome].
*/
const val DEBUG_DRAWER_HOME_ROUTE = "debug_drawer_home"
/**
* The home screen of the [DebugDrawer].
*
* @param destinations The list of [DebugDrawerDestination]s to display.
*/
@Composable
fun DebugDrawerHome(
destinations: List<DebugDrawerDestination>,
) {
val lazyListState = rememberLazyListState()
val appName: String
val appVersion: String
if (inComposePreview) {
appName = "App Name Preview"
appVersion = "100.00.000"
} else {
appName = LocalContext.current.appName
appVersion = LocalContext.current.appVersionName
}
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(color = FirefoxTheme.colors.layer1),
state = lazyListState,
) {
item(key = "home_header") {
Row(
modifier = Modifier
.padding(all = 16.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = appName,
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline5,
)
Text(
text = appVersion,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.headline5,
)
}
Divider()
}
items(
items = destinations,
key = { destination ->
destination.route
},
) { destination ->
TextListItem(
label = stringResource(id = destination.title),
onClick = destination.onClick,
)
Divider()
}
}
}
@Composable
@LightDarkPreview
private fun DebugDrawerHomePreview() {
val scope = rememberCoroutineScope()
val snackbarState = remember { SnackbarHostState() }
FirefoxTheme {
Box {
DebugDrawerHome(
destinations = List(size = 30) {
DebugDrawerDestination(
route = "screen_$it",
title = R.string.debug_drawer_title,
onClick = {
scope.launch {
snackbarState.showSnackbar("item $it clicked")
}
},
content = {},
)
},
)
SnackbarHost(
hostState = snackbarState,
modifier = Modifier.align(Alignment.BottomCenter),
) { snackbarData ->
Snackbar(
snackbarData = snackbarData,
)
}
}
}
}

@ -4,33 +4,78 @@
package org.mozilla.fenix.debugsettings.ui package org.mozilla.fenix.debugsettings.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.DrawerValue
import androidx.compose.material.ModalDrawer
import androidx.compose.material.Snackbar import androidx.compose.material.Snackbar
import androidx.compose.material.SnackbarHost import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.FloatingActionButton import org.mozilla.fenix.compose.button.FloatingActionButton
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerDestination
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
/** /**
* Overlay for presenting Fenix-wide debugging content. * Overlay for presenting app-wide debugging content.
*
* @param navController [NavHostController] used to perform navigation actions.
* @param drawerStatus The [DrawerStatus] indicating the physical state of the drawer.
* @param debugDrawerDestinations The complete list of [DebugDrawerDestination]s used to populate
* the [DebugDrawer] with sub screens.
* @param onDrawerOpen Invoked when the drawer is opened.
* @param onDrawerClose Invoked when the drawer is closed.
* @param onDrawerBackButtonClick Invoked when the user taps on the back button in the app bar.
*/ */
@Composable @Composable
fun DebugOverlay() { fun DebugOverlay(
navController: NavHostController,
drawerStatus: DrawerStatus,
debugDrawerDestinations: List<DebugDrawerDestination>,
onDrawerOpen: () -> Unit,
onDrawerClose: () -> Unit,
onDrawerBackButtonClick: () -> Unit,
) {
val snackbarState = remember { SnackbarHostState() } val snackbarState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
LaunchedEffect(drawerStatus) {
if (drawerStatus == DrawerStatus.Open) {
drawerState.open()
}
}
LaunchedEffect(drawerState) {
snapshotFlow { drawerState.currentValue }
.distinctUntilChanged()
.filter { it == DrawerValue.Closed }
.collect {
onDrawerClose()
}
}
Box( Box(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -41,12 +86,41 @@ fun DebugOverlay() {
.align(Alignment.CenterStart) .align(Alignment.CenterStart)
.padding(start = 16.dp), .padding(start = 16.dp),
onClick = { onClick = {
scope.launch { onDrawerOpen()
snackbarState.showSnackbar("Show debug drawer")
}
}, },
) )
// ModalDrawer utilizes a Surface, which blocks ALL clicks behind it, preventing the app
// from being interactable. This cannot be overridden in the Surface API, so we must hide
// the entire drawer when it is closed.
if (drawerStatus == DrawerStatus.Open) {
val currentLayoutDirection = LocalLayoutDirection.current
val sheetLayoutDirection = when (currentLayoutDirection) {
LayoutDirection.Rtl -> LayoutDirection.Ltr
LayoutDirection.Ltr -> LayoutDirection.Rtl
}
// Force the drawer to always open from the opposite side of the screen. We need to reset
// this below with `drawerContent` to ensure the content follows the correct direction.
CompositionLocalProvider(LocalLayoutDirection provides sheetLayoutDirection) {
ModalDrawer(
drawerContent = {
CompositionLocalProvider(LocalLayoutDirection provides currentLayoutDirection) {
DebugDrawer(
navController = navController,
destinations = debugDrawerDestinations,
onBackButtonClick = onDrawerBackButtonClick,
)
}
},
drawerBackgroundColor = FirefoxTheme.colors.layer1,
scrimColor = FirefoxTheme.colors.scrim,
drawerState = drawerState,
content = {},
)
}
}
// This must be the last element in the Box // This must be the last element in the Box
SnackbarHost( SnackbarHost(
hostState = snackbarState, hostState = snackbarState,
@ -62,13 +136,41 @@ fun DebugOverlay() {
@Composable @Composable
@LightDarkPreview @LightDarkPreview
private fun DebugOverlayPreview() { private fun DebugOverlayPreview() {
FirefoxTheme { val navController = rememberNavController()
Box( var drawerStatus by remember { mutableStateOf(DrawerStatus.Closed) }
modifier = Modifier val destinations = remember {
.fillMaxSize() List(size = 15) { index ->
.background(color = FirefoxTheme.colors.layer1), DebugDrawerDestination(
) { route = "screen_$index",
DebugOverlay() title = R.string.debug_drawer_title,
onClick = {
navController.navigate(route = "screen_$index")
},
content = {
Text(
text = "Tool $index",
color = FirefoxTheme.colors.textPrimary,
style = FirefoxTheme.typography.headline6,
)
},
)
} }
} }
FirefoxTheme {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = destinations,
onDrawerOpen = {
drawerStatus = DrawerStatus.Open
},
onDrawerClose = {
drawerStatus = DrawerStatus.Closed
},
onDrawerBackButtonClick = {
navController.popBackStack()
},
)
}
} }

@ -0,0 +1,83 @@
/* 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.debugsettings.ui
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.navigation.compose.rememberNavController
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.observeAsState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.debugsettings.navigation.DebugDrawerRoute
import org.mozilla.fenix.debugsettings.store.DebugDrawerAction
import org.mozilla.fenix.debugsettings.store.DebugDrawerNavigationMiddleware
import org.mozilla.fenix.debugsettings.store.DebugDrawerStore
import org.mozilla.fenix.debugsettings.store.DrawerStatus
import org.mozilla.fenix.debugsettings.tabs.TabTools
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Overlay for presenting Fenix-wide debugging content.
*
* @param browserStore [BrowserStore] used to access tab data for [TabTools].
* @param inactiveTabsEnabled Whether the inactive tabs feature is enabled.
*/
@Composable
fun FenixOverlay(
browserStore: BrowserStore,
inactiveTabsEnabled: Boolean,
) {
val navController = rememberNavController()
val coroutineScope = rememberCoroutineScope()
val debugDrawerStore = remember {
DebugDrawerStore(
middlewares = listOf(
DebugDrawerNavigationMiddleware(
navController = navController,
scope = coroutineScope,
),
),
)
}
val debugDrawerDestinations = remember {
DebugDrawerRoute.generateDebugDrawerDestinations(
debugDrawerStore = debugDrawerStore,
browserStore = browserStore,
inactiveTabsEnabled = inactiveTabsEnabled,
)
}
val drawerStatus by debugDrawerStore.observeAsState(initialValue = DrawerStatus.Closed) { state ->
state.drawerStatus
}
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
DebugOverlay(
navController = navController,
drawerStatus = drawerStatus,
debugDrawerDestinations = debugDrawerDestinations,
onDrawerOpen = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerOpened)
},
onDrawerClose = {
debugDrawerStore.dispatch(DebugDrawerAction.DrawerClosed)
},
onDrawerBackButtonClick = {
debugDrawerStore.dispatch(DebugDrawerAction.OnBackPressed)
},
)
}
}
@LightDarkPreview
@Composable
private fun FenixOverlayPreview() {
FenixOverlay(
browserStore = BrowserStore(),
inactiveTabsEnabled = true,
)
}

@ -70,7 +70,9 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi {
onFetchCallback = { onFetchCallback = {
context.settings().nimbusExperimentsFetched = true context.settings().nimbusExperimentsFetched = true
} }
}.build(appInfo) }.build(appInfo).also { nimbusApi ->
nimbusApi.recordIsReady(FxNimbus.features.nimbusIsReady.value().eventCount)
}
} }
private fun Context.reportError(message: String, e: Throwable) { private fun Context.reportError(message: String, e: Throwable) {

@ -15,12 +15,47 @@ import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.feature.intent.ext.getSessionId
import mozilla.components.feature.pwa.ext.getWebAppManifest
import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.customtabs.EXTRA_IS_SANDBOX_CUSTOM_TAB
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.settings.HttpsOnlyFragmentDirections
import org.mozilla.fenix.settings.SettingsFragmentDirections
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections
import org.mozilla.fenix.settings.about.AboutFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.SaveSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.SearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.settings.wallpaper.WallpaperSettingsFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.shopping.ReviewQualityCheckFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.translations.TranslationsDialogFragmentDirections
import java.security.InvalidParameterException
/** /**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons. * Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -170,7 +205,164 @@ fun Activity.setNavigationIcon(
} }
} }
/**
* Delegate to the relevant 'get nav directions' function based on the given [Activity].
*
* @param from The [BrowserDirection] to indicate which fragment the browser is being opened from.
* @param customTabSessionId Optional custom tab session ID if navigating from a custom tab.
*
* @return the [NavDirections] for the given [Activity].
*/
fun Activity.getNavDirections(
from: BrowserDirection,
customTabSessionId: String? = null,
): NavDirections? = when (this) {
is ExternalAppBrowserActivity -> {
getExternalAppBrowserNavDirections(from, customTabSessionId)
}
else -> {
getHomeNavDirections(from)
}
}
private fun Activity.getExternalAppBrowserNavDirections(
from: BrowserDirection,
customTabSessionId: String?,
): NavDirections? {
if (customTabSessionId == null) {
finishAndRemoveTask()
return null
}
val manifest =
intent.getWebAppManifest()?.let { WebAppManifestParser().serialize(it).toString() }
return when (from) {
BrowserDirection.FromGlobal ->
NavGraphDirections.actionGlobalExternalAppBrowser(
activeSessionId = customTabSessionId,
webAppManifest = manifest,
isSandboxCustomTab = intent.getBooleanExtra(EXTRA_IS_SANDBOX_CUSTOM_TAB, false),
)
else -> throw InvalidParameterException(
"Tried to navigate to ExternalAppBrowserFragment from $from",
)
}
}
private fun getHomeNavDirections(
from: BrowserDirection,
): NavDirections = when (from) {
BrowserDirection.FromGlobal -> NavGraphDirections.actionGlobalBrowser()
BrowserDirection.FromHome -> HomeFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromWallpaper -> WallpaperSettingsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSearchDialog -> SearchDialogFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSettings -> SettingsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromBookmarks -> BookmarkFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHistory -> HistoryFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHistoryMetadataGroup -> HistoryMetadataGroupFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromHttpsOnlyMode -> HttpsOnlyFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAbout -> AboutFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtection -> TrackingProtectionFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTrackingProtectionDialog ->
TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSavedLoginsFragment -> SavedLoginsAuthFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddNewDeviceFragment -> AddNewDeviceFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSearchEngineFragment -> SearchEngineFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromSaveSearchEngineFragment -> SaveSearchEngineFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonDetailsFragment -> AddonDetailsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonPermissionsDetailsFragment ->
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromLoginDetailFragment -> LoginDetailFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTabsTray -> TabsTrayFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromRecentlyClosed -> RecentlyClosedFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromReviewQualityCheck -> ReviewQualityCheckFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromAddonsManagementFragment -> AddonsManagementFragmentDirections.actionGlobalBrowser()
BrowserDirection.FromTranslationsDialogFragment -> TranslationsDialogFragmentDirections.actionGlobalBrowser()
}
const val REQUEST_CODE_BROWSER_ROLE = 1 const val REQUEST_CODE_BROWSER_ROLE = 1
const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key" const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args" const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
const val DEFAULT_BROWSER_APP_OPTION = "default_browser" const val DEFAULT_BROWSER_APP_OPTION = "default_browser"
const val EXTERNAL_APP_BROWSER_INTENT_SOURCE = "CUSTOM_TAB"
/**
* Depending on the [Activity], maybe derive the source of the given [intent].
*
* @param intent the [SafeIntent] to derive the source from.
*/
fun Activity.getIntentSource(intent: SafeIntent): String? = when (this) {
is ExternalAppBrowserActivity -> EXTERNAL_APP_BROWSER_INTENT_SOURCE
else -> getHomeIntentSource(intent)
}
private fun getHomeIntentSource(intent: SafeIntent): String? {
return when {
intent.isLauncherIntent -> HomeActivity.APP_ICON
intent.action == Intent.ACTION_VIEW -> "LINK"
else -> null
}
}
/**
* Depending on the [Activity], maybe derive the session ID of the given [intent].
*
* @param intent the [SafeIntent] to derive the session ID from.
*/
fun Activity.getIntentSessionId(intent: SafeIntent): String? = when (this) {
is ExternalAppBrowserActivity -> getExternalAppBrowserIntentSessionId(intent)
else -> null
}
private fun getExternalAppBrowserIntentSessionId(intent: SafeIntent) = intent.getSessionId()
/**
* Get the breadcrumb message for the [Activity].
*
* @param destination the [NavDestination] required to provide the destination ID.
*/
fun Activity.getBreadcrumbMessage(destination: NavDestination): String = when (this) {
is ExternalAppBrowserActivity -> getExternalAppBrowserBreadcrumbMessage(destination.id)
else -> getHomeBreadcrumbMessage(destination.id)
}
private fun Activity.getExternalAppBrowserBreadcrumbMessage(destinationId: Int): String {
val fragmentName = resources.getResourceEntryName(destinationId)
return "Changing to fragment $fragmentName, isCustomTab: true"
}
private fun Activity.getHomeBreadcrumbMessage(destinationId: Int): String {
val fragmentName = resources.getResourceEntryName(destinationId)
return "Changing to fragment $fragmentName, isCustomTab: false"
}

@ -19,7 +19,6 @@ import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
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.nimbus.FxNimbus
import org.mozilla.geckoview.ContentBlocking import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
@ -134,7 +133,7 @@ object GeckoProvider {
.consoleOutput(context.components.settings.enableGeckoLogs) .consoleOutput(context.components.settings.enableGeckoLogs)
.debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs) .debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs)
.aboutConfigEnabled(true) .aboutConfigEnabled(true)
.extensionsProcessEnabled(FxNimbus.features.extensionsProcess.value().enabled) .extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true) .extensionsWebAPIEnabled(true)
.build() .build()
} }

@ -49,7 +49,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
@ -119,7 +121,6 @@ import org.mozilla.fenix.messaging.FenixNimbusMessagingController
import org.mozilla.fenix.messaging.MessagingFeature import org.mozilla.fenix.messaging.MessagingFeature
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController import org.mozilla.fenix.search.toolbar.DefaultSearchSelectorController
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
@ -1008,17 +1009,18 @@ class HomeFragment : Fragment() {
lastAppliedWallpaperName = wallpaperName lastAppliedWallpaperName = wallpaperName
} }
else -> { else -> {
runBlockingIncrement { viewLifecycleOwner.lifecycleScope.launch {
// loadBitmap does file lookups based on name, so we don't need a fully // loadBitmap does file lookups based on name, so we don't need a fully
// qualified type to load the image // qualified type to load the image
val wallpaper = Wallpaper.Default.copy(name = wallpaperName) val wallpaper = Wallpaper.Default.copy(name = wallpaperName)
val wallpaperImage = val wallpaperImage =
requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper) context?.let { requireComponents.useCases.wallpaperUseCases.loadBitmap(it, wallpaper) }
wallpaperImage?.let { wallpaperImage?.let {
it.scaleToBottomOfView(binding.wallpaperImageView) it.scaleToBottomOfView(binding.wallpaperImageView)
binding.wallpaperImageView.isVisible = true binding.wallpaperImageView.isVisible = true
lastAppliedWallpaperName = wallpaperName lastAppliedWallpaperName = wallpaperName
} ?: run { } ?: run {
if (!isActive) return@run
with(binding.wallpaperImageView) { with(binding.wallpaperImageView) {
isVisible = false isVisible = false
showSnackBar( showSnackBar(
@ -1052,11 +1054,15 @@ class HomeFragment : Fragment() {
} }
private fun observeWallpaperUpdates() { private fun observeWallpaperUpdates() {
consumeFrom(requireComponents.appStore) { consumeFlow(requireComponents.appStore, viewLifecycleOwner) { flow ->
val currentWallpaper = it.wallpaperState.currentWallpaper flow.filter { it.mode == BrowsingMode.Normal }
if (currentWallpaper.name != lastAppliedWallpaperName) { .map { it.wallpaperState.currentWallpaper }
applyWallpaper(wallpaperName = currentWallpaper.name, orientationChange = false) .distinctUntilChanged()
} .collect {
if (it.name != lastAppliedWallpaperName) {
applyWallpaper(wallpaperName = it.name, orientationChange = false)
}
}
} }
} }

@ -47,7 +47,6 @@ import org.mozilla.fenix.GleanMetrics.HomeMenu as HomeMenuMetrics
* clicked. * clicked.
* @param fxaEntrypoint The source entry point to FxA. * @param fxaEntrypoint The source entry point to FxA.
*/ */
@Suppress("LongParameterList")
class HomeMenuView( class HomeMenuView(
private val view: View, private val view: View,
private val context: Context, private val context: Context,

@ -239,7 +239,7 @@ fun PocketSponsoredStory(
* @param onDiscoverMoreClicked Callback for when the user taps an element which contains an * @param onDiscoverMoreClicked Callback for when the user taps an element which contains an
*/ */
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList", "LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun PocketStories( fun PocketStories(
@PreviewParameter(PocketStoryProvider::class) stories: List<PocketStory>, @PreviewParameter(PocketStoryProvider::class) stories: List<PocketStory>,
@ -367,7 +367,6 @@ private fun alignColumnToTitlePadding(screenWidth: Dp, contentPadding: Dp) =
* @param onCategoryClick Callback for when the user taps a category. * @param onCategoryClick Callback for when the user taps a category.
*/ */
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList")
@Composable @Composable
fun PocketStoriesCategories( fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>, categories: List<PocketRecommendedStoriesCategory>,

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.recentbookmarks.view package org.mozilla.fenix.home.recentbookmarks.view
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -42,10 +41,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ContextualMenu import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
@ -172,6 +171,11 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
modifier = imageModifier, modifier = imageModifier,
targetSize = imageWidth, targetSize = imageWidth,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
fallback = {
if (!bookmark.url.isNullOrEmpty()) {
FallbackBookmarkFaviconImage(url = bookmark.url)
}
},
) )
} }
!bookmark.url.isNullOrEmpty() && !inComposePreview -> { !bookmark.url.isNullOrEmpty() && !inComposePreview -> {
@ -180,23 +184,7 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
PlaceholderBookmarkImage() PlaceholderBookmarkImage()
} }
WithIcon { icon -> FallbackBookmarkFaviconImage(bookmark.url)
Box(
modifier = imageModifier.background(
color = FirefoxTheme.colors.layer2,
),
contentAlignment = Alignment.Center,
) {
Image(
painter = icon.painter,
contentDescription = null,
modifier = Modifier
.size(36.dp)
.clip(cardShape),
contentScale = ContentScale.Crop,
)
}
}
} }
} }
inComposePreview -> { inComposePreview -> {
@ -217,6 +205,20 @@ private fun PlaceholderBookmarkImage() {
) )
} }
@Composable
private fun FallbackBookmarkFaviconImage(
url: String,
) {
Box(
modifier = imageModifier.background(
color = FirefoxTheme.colors.layer2,
),
contentAlignment = Alignment.Center,
) {
Favicon(url = url, size = 36.dp)
}
}
@Composable @Composable
@LightDarkPreview @LightDarkPreview
private fun RecentBookmarksPreview() { private fun RecentBookmarksPreview() {

@ -69,7 +69,7 @@ private const val THUMBNAIL_SIZE = 108
* @param onRemoveSyncedTab Invoked when user clicks on the "Remove" dropdown menu option. * @param onRemoveSyncedTab Invoked when user clicks on the "Remove" dropdown menu option.
*/ */
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList") @Suppress("LongMethod")
@Composable @Composable
fun RecentSyncedTab( fun RecentSyncedTab(
tab: RecentSyncedTab?, tab: RecentSyncedTab?,

@ -240,6 +240,15 @@ fun RecentTabImage(
modifier = modifier, modifier = modifier,
targetSize = THUMBNAIL_SIZE.dp, targetSize = THUMBNAIL_SIZE.dp,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
fallback = {
TabThumbnail(
tab = tab.state,
size = LocalDensity.current.run { THUMBNAIL_SIZE.dp.toPx().toInt() },
storage = storage,
modifier = modifier,
contentScale = contentScale,
)
},
) )
} }
else -> TabThumbnail( else -> TabThumbnail(

@ -190,7 +190,6 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback<AdapterItem>() {
} }
} }
@Suppress("LongParameterList")
class SessionControlAdapter( class SessionControlAdapter(
private val interactor: SessionControlInteractor, private val interactor: SessionControlInteractor,
private val viewLifecycleOwner: LifecycleOwner, private val viewLifecycleOwner: LifecycleOwner,

@ -239,7 +239,7 @@ data class TopSiteColors(
* @param onTopSiteLongClick Invoked when the user long clicks on a top site. * @param onTopSiteLongClick Invoked when the user long clicks on a top site.
* @param onTopSitesItemBound Invoked during the composition of a top site item. * @param onTopSitesItemBound Invoked during the composition of a top site item.
*/ */
@Suppress("LongParameterList", "LongMethod") @Suppress("LongMethod")
@OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
@Composable @Composable
private fun TopSiteItem( private fun TopSiteItem(
@ -401,7 +401,6 @@ private fun TopSiteFavicon(url: String, imageUrl: String? = null) {
} }
@Composable @Composable
@Suppress("LongParameterList")
private fun getMenuItems( private fun getMenuItems(
topSite: TopSite, topSite: TopSite,
onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit, onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit,

@ -54,7 +54,6 @@ class HistoryListItemViewHolder(
* @param groupPendingDeletionCount allows to properly display the number of items inside a * @param groupPendingDeletionCount allows to properly display the number of items inside a
* history group, taking into account pending removal of items inside. * history group, taking into account pending removal of items inside.
*/ */
@Suppress("LongParameterList")
fun bind( fun bind(
item: History, item: History,
timeGroup: HistoryItemTimeGroup?, timeGroup: HistoryItemTimeGroup?,

@ -4,26 +4,26 @@
package org.mozilla.fenix.messaging package org.mozilla.fenix.messaging
import android.app.Activity
import android.app.Notification import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import androidx.activity.ComponentActivity
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import kotlinx.coroutines.launch
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Message import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.support.base.ids.SharedIdsHelper import mozilla.components.support.base.ids.SharedIdsHelper
import mozilla.components.support.utils.BootUtils import mozilla.components.support.utils.BootUtils
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification import org.mozilla.fenix.utils.createBaseNotification
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -170,28 +170,27 @@ class MessageNotificationWorker(
* When a [Message] [Notification] is dismissed by the user record telemetry data and update the * When a [Message] [Notification] is dismissed by the user record telemetry data and update the
* [Message.metadata]. * [Message.metadata].
* *
* This [Service] is only intended to be used by the [MessageNotificationWorker.createOnDismissPendingIntent] function. * This Service is only intended to be used by the [MessageNotificationWorker.createOnDismissPendingIntent] function.
*/ */
class NotificationDismissedService : Service() { class NotificationDismissedService : LifecycleService() {
/**
* This service cannot be bound to.
*/
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (intent != null) { if (intent != null) {
val nimbusMessagingController = val nimbusMessagingController =
FenixNimbusMessagingController(applicationContext.components.analytics.messagingStorage) FenixNimbusMessagingController(applicationContext.components.analytics.messagingStorage)
// Get the relevant message. lifecycleScope.launch {
val message = intent.getStringExtra(DISMISSED_MESSAGE_ID)?.let { messageId -> // Get the relevant message.
runBlockingIncrement { nimbusMessagingController.getMessage(messageId) } val message = intent.getStringExtra(DISMISSED_MESSAGE_ID)?.let { messageId ->
} nimbusMessagingController.getMessage(messageId)
}
if (message != null) { if (message != null) {
// Update message as 'dismissed'. // Update message as 'dismissed'.
runBlockingIncrement { nimbusMessagingController.onMessageDismissed(message.metadata) } nimbusMessagingController.onMessageDismissed(message.metadata)
}
} }
} }
@ -203,9 +202,9 @@ class NotificationDismissedService : Service() {
* When a [Message] [Notification] is clicked by the user record telemetry data and update the * When a [Message] [Notification] is clicked by the user record telemetry data and update the
* [Message.metadata]. * [Message.metadata].
* *
* This [Activity] is only intended to be used by the [MessageNotificationWorker.createOnClickPendingIntent] function. * This Activity is only intended to be used by the [MessageNotificationWorker.createOnClickPendingIntent] function.
*/ */
class NotificationClickedReceiverActivity : Activity() { class NotificationClickedReceiverActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -213,22 +212,24 @@ class NotificationClickedReceiverActivity : Activity() {
val nimbusMessagingController = val nimbusMessagingController =
FenixNimbusMessagingController(components.analytics.messagingStorage) FenixNimbusMessagingController(components.analytics.messagingStorage)
// Get the relevant message. lifecycleScope.launch {
val message = intent.getStringExtra(CLICKED_MESSAGE_ID)?.let { messageId -> // Get the relevant message.
runBlockingIncrement { nimbusMessagingController.getMessage(messageId) } val message = intent.getStringExtra(CLICKED_MESSAGE_ID)?.let { messageId ->
} nimbusMessagingController.getMessage(messageId)
}
if (message != null) { if (message != null) {
// Update message as 'clicked'. // Update message as 'clicked'.
runBlockingIncrement { nimbusMessagingController.onMessageClicked(message.metadata) } nimbusMessagingController.onMessageClicked(message.metadata)
// Create the intent. // Create the intent.
val intent = nimbusMessagingController.getIntentForMessage(message) val intent = nimbusMessagingController.getIntentForMessage(message)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// Start the message intent. // Start the message intent.
startActivity(intent) startActivity(intent)
}
} }
// End this activity. // End this activity.

@ -18,21 +18,23 @@ import android.view.ViewGroup
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import mozilla.components.service.nimbus.evalJexlSafe import mozilla.components.service.nimbus.evalJexlSafe
import mozilla.components.support.base.ext.areNotificationsEnabledSafe import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import mozilla.components.support.utils.BrowsersCache
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.compose.LinkTextState
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.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.view.Caption
import org.mozilla.fenix.onboarding.view.OnboardingPageUiData import org.mozilla.fenix.onboarding.view.OnboardingPageUiData
import org.mozilla.fenix.onboarding.view.OnboardingScreen import org.mozilla.fenix.onboarding.view.OnboardingScreen
import org.mozilla.fenix.onboarding.view.sequencePosition import org.mozilla.fenix.onboarding.view.sequencePosition
@ -49,6 +51,7 @@ class OnboardingFragment : Fragment() {
private val pagesToDisplay by lazy { private val pagesToDisplay by lazy {
pagesToDisplay( pagesToDisplay(
shouldShowDefaultBrowserCard(requireContext()),
canShowNotificationPage(requireContext()), canShowNotificationPage(requireContext()),
canShowAddWidgetCard(), canShowAddWidgetCard(),
) )
@ -59,6 +62,11 @@ class OnboardingFragment : Fragment() {
@SuppressLint("SourceLockedOrientationActivity") @SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (pagesToDisplay.isEmpty()) {
/* do not continue if there's no onboarding pages to display */
onFinish(null)
}
if (isNotATablet()) { if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
} }
@ -97,7 +105,6 @@ class OnboardingFragment : Fragment() {
@Composable @Composable
@Suppress("LongMethod") @Suppress("LongMethod")
private fun ScreenContent() { private fun ScreenContent() {
val context = LocalContext.current
OnboardingScreen( OnboardingScreen(
pagesToDisplay = pagesToDisplay, pagesToDisplay = pagesToDisplay,
onMakeFirefoxDefaultClick = { onMakeFirefoxDefaultClick = {
@ -113,18 +120,6 @@ class OnboardingFragment : Fragment() {
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER), pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
) )
}, },
onPrivacyPolicyClick = { url ->
startActivity(
SupportUtils.createSandboxCustomTabIntent(
context = context,
url = url,
),
)
telemetryRecorder.onPrivacyPolicyClick(
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
)
},
onSignInButtonClick = { onSignInButtonClick = {
findNavController().nav( findNavController().nav(
id = R.id.onboardingFragment, id = R.id.onboardingFragment,
@ -172,10 +167,7 @@ class OnboardingFragment : Fragment() {
) )
}, },
onFinish = { onFinish = {
onFinish( onFinish(it)
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition = pagesToDisplay.sequencePosition(it.type),
)
}, },
onImpression = { onImpression = {
telemetryRecorder.onImpression( telemetryRecorder.onImpression(
@ -200,18 +192,28 @@ class OnboardingFragment : Fragment() {
} }
} }
private fun onFinish(sequenceId: String, sequencePosition: String) { private fun onFinish(onboardingPageUiData: OnboardingPageUiData?) {
/* onboarding page UI data can be null if there was no pages to display */
if (onboardingPageUiData != null) {
val sequenceId = pagesToDisplay.telemetrySequenceId()
val sequencePosition = pagesToDisplay.sequencePosition(onboardingPageUiData.type)
telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId,
sequencePosition = sequencePosition,
)
}
requireComponents.fenixOnboarding.finish() requireComponents.fenixOnboarding.finish()
findNavController().nav( findNavController().nav(
id = R.id.onboardingFragment, id = R.id.onboardingFragment,
directions = OnboardingFragmentDirections.actionHome(), directions = OnboardingFragmentDirections.actionHome(),
) )
telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId,
sequencePosition = sequencePosition,
)
} }
private fun shouldShowDefaultBrowserCard(context: Context) =
!BrowsersCache.all(context.applicationContext).isDefaultBrowser
private fun canShowNotificationPage(context: Context) = private fun canShowNotificationPage(context: Context) =
!NotificationManagerCompat.from(context.applicationContext) !NotificationManagerCompat.from(context.applicationContext)
.areNotificationsEnabledSafe() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU .areNotificationsEnabledSafe() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
@ -221,13 +223,35 @@ class OnboardingFragment : Fragment() {
private fun isNotATablet() = !resources.getBoolean(R.bool.tablet) private fun isNotATablet() = !resources.getBoolean(R.bool.tablet)
private fun pagesToDisplay( private fun pagesToDisplay(
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean, showNotificationPage: Boolean,
showAddWidgetPage: Boolean, showAddWidgetPage: Boolean,
): List<OnboardingPageUiData> { ): List<OnboardingPageUiData> {
val jexlConditions = FxNimbus.features.junoOnboarding.value().conditions val jexlConditions = FxNimbus.features.junoOnboarding.value().conditions
val jexlHelper = requireContext().components.analytics.messagingStorage.helper val jexlHelper = requireContext().components.analytics.messagingStorage.helper
val privacyCaption = Caption(
text = getString(R.string.juno_onboarding_privacy_notice_text),
linkTextState = LinkTextState(
text = getString(R.string.juno_onboarding_privacy_notice_text),
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = {
startActivity(
SupportUtils.createSandboxCustomTabIntent(
context = requireContext(),
url = it,
),
)
telemetryRecorder.onPrivacyPolicyClick(
pagesToDisplay.telemetrySequenceId(),
pagesToDisplay.sequencePosition(OnboardingPageUiData.Type.DEFAULT_BROWSER),
)
},
),
)
return FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData( return FxNimbus.features.junoOnboarding.value().cards.values.toPageUiData(
privacyCaption,
showDefaultBrowserPage,
showNotificationPage, showNotificationPage,
showAddWidgetPage, showAddWidgetPage,
jexlConditions, jexlConditions,

@ -4,15 +4,15 @@
package org.mozilla.fenix.onboarding.view package org.mozilla.fenix.onboarding.view
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.nimbus.OnboardingCardData import org.mozilla.fenix.nimbus.OnboardingCardData
import org.mozilla.fenix.nimbus.OnboardingCardType import org.mozilla.fenix.nimbus.OnboardingCardType
import org.mozilla.fenix.settings.SupportUtils
/** /**
* Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData]. * Returns a list of all the required Nimbus 'cards' that have been converted to [OnboardingPageUiData].
*/ */
internal fun Collection<OnboardingCardData>.toPageUiData( internal fun Collection<OnboardingCardData>.toPageUiData(
privacyCaption: Caption,
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean, showNotificationPage: Boolean,
showAddWidgetPage: Boolean, showAddWidgetPage: Boolean,
jexlConditions: Map<String, String>, jexlConditions: Map<String, String>,
@ -21,16 +21,25 @@ internal fun Collection<OnboardingCardData>.toPageUiData(
// we are first filtering the cards based on Nimbus configuration // we are first filtering the cards based on Nimbus configuration
return filter { it.shouldDisplayCard(func, jexlConditions) } return filter { it.shouldDisplayCard(func, jexlConditions) }
// we are then filtering again based on device capabilities // we are then filtering again based on device capabilities
.filter { it.isCardEnabled(showNotificationPage, showAddWidgetPage) } .filter { it.isCardEnabled(showDefaultBrowserPage, showNotificationPage, showAddWidgetPage) }
.sortedBy { it.ordering } .sortedBy { it.ordering }
.map { it.toPageUiData() } .mapIndexed {
index, onboardingCardData ->
// only first onboarding card shows privacy caption
onboardingCardData.toPageUiData(if (index == 0) privacyCaption else null)
}
} }
private fun OnboardingCardData.isCardEnabled( private fun OnboardingCardData.isCardEnabled(
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean, showNotificationPage: Boolean,
showAddWidgetPage: Boolean, showAddWidgetPage: Boolean,
): Boolean = ): Boolean =
when (cardType) { when (cardType) {
OnboardingCardType.DEFAULT_BROWSER -> {
enabled && showDefaultBrowserPage
}
OnboardingCardType.NOTIFICATION_PERMISSION -> { OnboardingCardType.NOTIFICATION_PERMISSION -> {
enabled && showNotificationPage enabled && showNotificationPage
} }
@ -91,14 +100,14 @@ private fun OnboardingCardData.shouldDisplayCard(
return validPrerequisites && !hasDisqualifiers return validPrerequisites && !hasDisqualifiers
} }
private fun OnboardingCardData.toPageUiData() = OnboardingPageUiData( private fun OnboardingCardData.toPageUiData(privacyCaption: Caption?) = OnboardingPageUiData(
type = cardType.toPageUiDataType(), type = cardType.toPageUiDataType(),
imageRes = imageRes.resourceId, imageRes = imageRes.resourceId,
title = title, title = title,
description = body, description = body,
linkText = linkText,
primaryButtonLabel = primaryButtonLabel, primaryButtonLabel = primaryButtonLabel,
secondaryButtonLabel = secondaryButtonLabel, secondaryButtonLabel = secondaryButtonLabel,
privacyCaption = privacyCaption,
) )
private fun OnboardingCardType.toPageUiDataType() = when (this) { private fun OnboardingCardType.toPageUiDataType() = when (this) {
@ -117,7 +126,6 @@ internal fun mapToOnboardingPageState(
onboardingPageUiData: OnboardingPageUiData, onboardingPageUiData: OnboardingPageUiData,
onMakeFirefoxDefaultClick: () -> Unit, onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit, onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onSignInButtonClick: () -> Unit, onSignInButtonClick: () -> Unit,
onSignInSkipClick: () -> Unit, onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit, onNotificationPermissionButtonClick: () -> Unit,
@ -129,14 +137,12 @@ internal fun mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData, onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onMakeFirefoxDefaultClick, onPositiveButtonClick = onMakeFirefoxDefaultClick,
onNegativeButtonClick = onMakeFirefoxDefaultSkipClick, onNegativeButtonClick = onMakeFirefoxDefaultSkipClick,
onUrlClick = onPrivacyPolicyClick,
) )
OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> createOnboardingPageState( OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData, onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onAddFirefoxWidgetClick, onPositiveButtonClick = onAddFirefoxWidgetClick,
onNegativeButtonClick = onAddFirefoxWidgetSkipClick, onNegativeButtonClick = onAddFirefoxWidgetSkipClick,
onUrlClick = onPrivacyPolicyClick,
) )
OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState( OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState(
@ -156,18 +162,11 @@ private fun createOnboardingPageState(
onboardingPageUiData: OnboardingPageUiData, onboardingPageUiData: OnboardingPageUiData,
onPositiveButtonClick: () -> Unit, onPositiveButtonClick: () -> Unit,
onNegativeButtonClick: () -> Unit, onNegativeButtonClick: () -> Unit,
onUrlClick: (String) -> Unit = {},
): OnboardingPageState = OnboardingPageState( ): OnboardingPageState = OnboardingPageState(
imageRes = onboardingPageUiData.imageRes, imageRes = onboardingPageUiData.imageRes,
title = onboardingPageUiData.title, title = onboardingPageUiData.title,
description = onboardingPageUiData.description, description = onboardingPageUiData.description,
linkTextState = onboardingPageUiData.linkText?.let {
LinkTextState(
text = it,
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = onUrlClick,
)
},
primaryButton = Action(onboardingPageUiData.primaryButtonLabel, onPositiveButtonClick), primaryButton = Action(onboardingPageUiData.primaryButtonLabel, onPositiveButtonClick),
secondaryButton = Action(onboardingPageUiData.secondaryButtonLabel, onNegativeButtonClick), secondaryButton = Action(onboardingPageUiData.secondaryButtonLabel, onNegativeButtonClick),
privacyCaption = onboardingPageUiData.privacyCaption,
) )

@ -30,7 +30,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.LinkText import org.mozilla.fenix.compose.LinkText
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton import org.mozilla.fenix.compose.button.PrimaryButton
import org.mozilla.fenix.compose.button.SecondaryButton import org.mozilla.fenix.compose.button.SecondaryButton
@ -61,6 +60,7 @@ private const val IMAGE_HEIGHT_RATIO_SMALL = 0.28f
* it doesn't show the close button. * it doesn't show the close button.
*/ */
@Composable @Composable
@Suppress("LongMethod")
fun OnboardingPage( fun OnboardingPage(
pageState: OnboardingPageState, pageState: OnboardingPageState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@ -116,10 +116,21 @@ fun OnboardingPage(
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
DescriptionText( Text(
description = pageState.description, text = pageState.description,
linkTextState = pageState.linkTextState, color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
) )
Spacer(modifier = Modifier.height(16.dp))
pageState.privacyCaption?.let { privacyCaption ->
LinkText(
text = privacyCaption.text,
linkTextStates = listOf(privacyCaption.linkTextState),
)
}
} }
Column( Column(
@ -147,26 +158,6 @@ fun OnboardingPage(
} }
} }
@Composable
private fun DescriptionText(
description: String,
linkTextState: LinkTextState?,
) {
if (linkTextState != null && description.contains(linkTextState.text, ignoreCase = true)) {
LinkText(
text = description,
linkTextStates = listOf(linkTextState),
)
} else {
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
textAlign = TextAlign.Center,
style = FirefoxTheme.typography.body2,
)
}
}
/** /**
* Calculates the image height to be set. The ratio is selected based on parent height. * Calculates the image height to be set. The ratio is selected based on parent height.
*/ */

@ -13,7 +13,7 @@ import org.mozilla.fenix.compose.LinkTextState
* @property imageRes [DrawableRes] displayed on the page. * @property imageRes [DrawableRes] displayed on the page.
* @property title [String] title of the page. * @property title [String] title of the page.
* @property description [String] description of the page. * @property description [String] description of the page.
* @property linkTextState [LinkTextState] part of description text with a link. * @property privacyCaption privacy caption to show and allow user to view on privacy policy.
* @property primaryButton [Action] action for the primary button. * @property primaryButton [Action] action for the primary button.
* @property secondaryButton [Action] action for the secondary button. * @property secondaryButton [Action] action for the secondary button.
* @property onRecordImpressionEvent Callback for recording impression event. * @property onRecordImpressionEvent Callback for recording impression event.
@ -22,7 +22,7 @@ data class OnboardingPageState(
@DrawableRes val imageRes: Int, @DrawableRes val imageRes: Int,
val title: String, val title: String,
val description: String, val description: String,
val linkTextState: LinkTextState? = null, val privacyCaption: Caption? = null,
val primaryButton: Action, val primaryButton: Action,
val secondaryButton: Action? = null, val secondaryButton: Action? = null,
val onRecordImpressionEvent: () -> Unit = {}, val onRecordImpressionEvent: () -> Unit = {},
@ -35,3 +35,11 @@ data class Action(
val text: String, val text: String,
val onClick: () -> Unit, val onClick: () -> Unit,
) )
/**
* Model containing text and [LinkTextState] for a caption.
*/
data class Caption(
val text: String,
val linkTextState: LinkTextState,
)

@ -15,9 +15,9 @@ data class OnboardingPageUiData(
@DrawableRes val imageRes: Int, @DrawableRes val imageRes: Int,
val title: String, val title: String,
val description: String, val description: String,
val linkText: String? = null,
val primaryButtonLabel: String, val primaryButtonLabel: String,
val secondaryButtonLabel: String, val secondaryButtonLabel: String,
val privacyCaption: Caption?,
) { ) {
/** /**
* Model for different types of Onboarding Pages. * Model for different types of Onboarding Pages.

@ -37,6 +37,7 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.compose.PagerIndicator import org.mozilla.fenix.compose.PagerIndicator
import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.onboarding.WidgetPinnedReceiver.WidgetPinnedState import org.mozilla.fenix.onboarding.WidgetPinnedReceiver.WidgetPinnedState
@ -48,7 +49,6 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param pagesToDisplay List of pages to be displayed in onboarding pager ui. * @param pagesToDisplay List of pages to be displayed in onboarding pager ui.
* @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked. * @param onMakeFirefoxDefaultClick Invoked when positive button on default browser page is clicked.
* @param onSkipDefaultClick Invoked when negative button on default browser page is clicked. * @param onSkipDefaultClick Invoked when negative button on default browser page is clicked.
* @param onPrivacyPolicyClick Invoked when the privacy policy link text is clicked.
* @param onSignInButtonClick Invoked when the positive button on the sign in page is clicked. * @param onSignInButtonClick Invoked when the positive button on the sign in page is clicked.
* @param onSkipSignInClick Invoked when the negative button on the sign in page is clicked. * @param onSkipSignInClick Invoked when the negative button on the sign in page is clicked.
* @param onNotificationPermissionButtonClick Invoked when positive button on notification page is * @param onNotificationPermissionButtonClick Invoked when positive button on notification page is
@ -65,7 +65,6 @@ fun OnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>, pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit, onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit, onSkipDefaultClick: () -> Unit,
onPrivacyPolicyClick: (url: String) -> Unit,
onSignInButtonClick: () -> Unit, onSignInButtonClick: () -> Unit,
onSkipSignInClick: () -> Unit, onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit, onNotificationPermissionButtonClick: () -> Unit,
@ -127,9 +126,6 @@ fun OnboardingScreen(
scrollToNextPageOrDismiss() scrollToNextPageOrDismiss()
onSkipDefaultClick() onSkipDefaultClick()
}, },
onPrivacyPolicyClick = {
onPrivacyPolicyClick(it)
},
onSignInButtonClick = { onSignInButtonClick = {
onSignInButtonClick() onSignInButtonClick()
scrollToNextPageOrDismiss() scrollToNextPageOrDismiss()
@ -167,7 +163,6 @@ private fun OnboardingContent(
pagerState: PagerState, pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit, onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit, onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onSignInButtonClick: () -> Unit, onSignInButtonClick: () -> Unit,
onSignInSkipClick: () -> Unit, onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit, onNotificationPermissionButtonClick: () -> Unit,
@ -195,7 +190,6 @@ private fun OnboardingContent(
onboardingPageUiData = pageUiState, onboardingPageUiData = pageUiState,
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick, onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
onMakeFirefoxDefaultSkipClick = onMakeFirefoxDefaultSkipClick, onMakeFirefoxDefaultSkipClick = onMakeFirefoxDefaultSkipClick,
onPrivacyPolicyClick = onPrivacyPolicyClick,
onSignInButtonClick = onSignInButtonClick, onSignInButtonClick = onSignInButtonClick,
onSignInSkipClick = onSignInSkipClick, onSignInSkipClick = onSignInSkipClick,
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick, onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
@ -251,7 +245,6 @@ private fun OnboardingScreenPreview() {
}, },
onMakeFirefoxDefaultClick = {}, onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {}, onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = {},
onSignInButtonClick = {}, onSignInButtonClick = {},
onSignInSkipClick = {}, onSignInSkipClick = {},
onNotificationPermissionButtonClick = {}, onNotificationPermissionButtonClick = {},
@ -268,10 +261,17 @@ private fun defaultPreviewPages() = listOf(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER, type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome, imageRes = R.drawable.ic_onboarding_welcome,
title = stringResource(R.string.juno_onboarding_default_browser_title_nimbus_2), title = stringResource(R.string.juno_onboarding_default_browser_title_nimbus_2),
description = stringResource(R.string.juno_onboarding_default_browser_description_nimbus_2), description = stringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
linkText = stringResource(R.string.juno_onboarding_default_browser_description_link_text),
primaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_positive_button), primaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_negative_button), secondaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_negative_button),
privacyCaption = Caption(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
linkTextState = LinkTextState(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
url = "",
onClick = {},
),
),
), ),
OnboardingPageUiData( OnboardingPageUiData(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN, type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
@ -280,6 +280,14 @@ private fun defaultPreviewPages() = listOf(
description = stringResource(R.string.juno_onboarding_sign_in_description_2), description = stringResource(R.string.juno_onboarding_sign_in_description_2),
primaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_positive_button), primaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_negative_button), secondaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_negative_button),
privacyCaption = Caption(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
linkTextState = LinkTextState(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
url = "",
onClick = {},
),
),
), ),
OnboardingPageUiData( OnboardingPageUiData(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION, type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
@ -288,5 +296,13 @@ private fun defaultPreviewPages() = listOf(
description = stringResource(R.string.juno_onboarding_enable_notifications_description_nimbus_2), description = stringResource(R.string.juno_onboarding_enable_notifications_description_nimbus_2),
primaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_positive_button), primaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_positive_button),
secondaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_negative_button), secondaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_negative_button),
privacyCaption = Caption(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
linkTextState = LinkTextState(
text = stringResource(R.string.juno_onboarding_privacy_notice_text),
url = "",
onClick = {},
),
),
), ),
) )

@ -672,6 +672,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
}.show() }.show()
} }
} }
Events.browserToolbarQrScanCompleted.record()
}, },
) )
} }

@ -141,7 +141,6 @@ data class SearchFragmentState(
/** /**
* Creates the initial state for the search fragment. * Creates the initial state for the search fragment.
*/ */
@Suppress("LongParameterList")
fun createInitialSearchFragmentState( fun createInitialSearchFragmentState(
activity: HomeActivity, activity: HomeActivity,
components: Components, components: Components,

@ -13,6 +13,7 @@ import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.search.SearchEngineSource import org.mozilla.fenix.search.SearchEngineSource
@ -116,6 +117,10 @@ class ToolbarView(
url = text url = text
interactor.onTextChanged(text) interactor.onTextChanged(text)
} }
override fun onInputCleared() {
Events.browserToolbarInputCleared.record()
}
}, },
) )
} }

@ -6,6 +6,7 @@ package org.mozilla.fenix.session
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
@ -35,17 +36,22 @@ class PrivateNotificationService : AbstractPrivateNotificationService() {
override fun NotificationCompat.Builder.buildNotification() { override fun NotificationCompat.Builder.buildNotification() {
setSmallIcon(R.drawable.ic_private_browsing) setSmallIcon(R.drawable.ic_private_browsing)
setContentTitle(
applicationContext.getString( val contentTitle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
R.string.app_name_private_4, applicationContext.getString(R.string.notification_erase_title_android_14)
getString(R.string.app_name), } else {
), applicationContext.getString(R.string.app_name_private_4, getString(R.string.app_name))
) }
setContentText(
applicationContext.getString( val contentText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
R.string.notification_pbm_delete_text_2, getString(R.string.notification_erase_text_android_14)
), } else {
) getString(R.string.notification_pbm_delete_text_2)
}
setContentTitle(contentTitle)
setContentText(contentText)
color = ContextCompat.getColor( color = ContextCompat.getColor(
this@PrivateNotificationService, this@PrivateNotificationService,
R.color.pbm_notification_color, R.color.pbm_notification_color,

@ -59,6 +59,7 @@ object SupportUtils {
UNSIGNED_ADDONS("unsigned-addons"), UNSIGNED_ADDONS("unsigned-addons"),
REVIEW_QUALITY_CHECK("review_checker_mobile"), REVIEW_QUALITY_CHECK("review_checker_mobile"),
FX_SUGGEST("search-suggestions-firefox"), FX_SUGGEST("search-suggestions-firefox"),
TRANSLATIONS("android-translation"),
} }
enum class MozillaPage(internal val path: String) { enum class MozillaPage(internal val path: String) {

@ -30,7 +30,6 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
* @param onReconnectClicked A callback executed when the [syncPreference] is clicked with a * @param onReconnectClicked A callback executed when the [syncPreference] is clicked with a
* preference status of "Reconnect". * preference status of "Reconnect".
*/ */
@Suppress("LongParameterList")
class SyncPreferenceView( class SyncPreferenceView(
private val syncPreference: SyncPreference, private val syncPreference: SyncPreference,
lifecycleOwner: LifecycleOwner, lifecycleOwner: LifecycleOwner,

@ -136,7 +136,8 @@ class AboutFragment : Fragment(), AboutPageListener {
WHATS_NEW, WHATS_NEW,
SupportUtils.WHATS_NEW_URL, SupportUtils.WHATS_NEW_URL,
), ),
getString(R.string.about_whats_new, getString(R.string.app_name)), // Note: Fenix only has release notes for 'Release' versions, NOT 'Beta' & 'Nightly'.
getString(R.string.about_whats_new, getString(R.string.firefox)),
), ),
AboutPageItem( AboutPageItem(
AboutItem.ExternalLink( AboutItem.ExternalLink(

@ -22,6 +22,7 @@ import org.mozilla.fenix.utils.Settings
* @param loginsFragmentStore Store used to hold in-memory collection state. * @param loginsFragmentStore Store used to hold in-memory collection state.
* @param navController NavController manages app navigation within a NavHost. * @param navController NavController manages app navigation within a NavHost.
* @param browserNavigator Controller allowing browser navigation to any Uri. * @param browserNavigator Controller allowing browser navigation to any Uri.
* @param addLoginCallback Callback used for add login
* @param settings SharedPreferences wrapper for easier usage. * @param settings SharedPreferences wrapper for easier usage.
*/ */
class LoginsListController( class LoginsListController(
@ -32,6 +33,7 @@ class LoginsListController(
newTab: Boolean, newTab: Boolean,
from: BrowserDirection, from: BrowserDirection,
) -> Unit, ) -> Unit,
private val addLoginCallback: () -> Unit,
private val settings: Settings, private val settings: Settings,
) { ) {
@ -46,6 +48,7 @@ class LoginsListController(
fun handleAddLoginClicked() { fun handleAddLoginClicked() {
Logins.managementAddTapped.record(NoExtras()) Logins.managementAddTapped.record(NoExtras())
addLoginCallback.invoke()
navController.navigate( navController.navigate(
SavedLoginsFragmentDirections.actionSavedLoginsFragmentToAddLoginFragment(), SavedLoginsFragmentDirections.actionSavedLoginsFragmentToAddLoginFragment(),
) )

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save