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,
"deepLinkScheme": deepLinkSchemeValue
]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
}
def releaseTemplate = {
@ -239,7 +241,14 @@ android {
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 {
buildConfigField "boolean", "LEAKCANARY", "false"
}
}
// Generate Kotlin code for the Fenix Glean metrics.
@ -670,13 +678,16 @@ dependencies {
implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_compose_navigation
implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_lifecycle_service
implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx
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 {
// 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.
ext.gleanExpireByVersion = Config.majorVersion()

@ -135,12 +135,12 @@ events:
description: |
A string containing the name of the item the user tapped. These items
include:
add_to_homescreen, add_to_top_sites, addons_manager, back, bookmark,
bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, history, new_tab, open_in_app, open_in_fenix,
quit, reader_mode_appearance, reload, remove_from_top_sites,
add_to_homescreen, add_to_top_sites, addons_manager, back, back_long_press,
bookmark, bookmarks, desktop_view_off, desktop_view_on, downloads,
find_in_page, forward, forward_long_press, history, new_tab, open_in_app,
open_in_fenix, quit, reader_mode_appearance, reload, remove_from_top_sites,
save_to_collection, set_default_browser, settings, share, stop,
sync_account, and print_content.
sync_account, translate and print_content.
type: string
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1024
@ -475,6 +475,23 @@ events:
notification_emails:
- android-probes@mozilla.com
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:
type: event
description: |
@ -489,6 +506,22 @@ events:
notification_emails:
- android-probes@mozilla.com
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:
type: event
description: |
@ -506,6 +539,22 @@ events:
metadata:
tags:
- 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:
type: event
description: |

@ -214,14 +214,6 @@ features:
type: Boolean
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:
description: A feature measuring campaign growth data
variables:
@ -341,6 +333,26 @@ features:
type: Map<String, String>
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:
description: A feature that provides Firefox Suggest search suggestions.
variables:
@ -361,6 +373,17 @@ features:
- channel: nightly
value:
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:
objects: {}

@ -25,8 +25,7 @@ features:
card-type: default-browser
title: juno_onboarding_default_browser_title_nimbus_2
ordering: 10
body: juno_onboarding_default_browser_description_nimbus_2
link-text: juno_onboarding_default_browser_description_link_text
body: juno_onboarding_default_browser_description_nimbus_3
image-res: ic_onboarding_welcome
primary-button-label: juno_onboarding_default_browser_positive_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.
# This should never be defaulted.
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:
type: Image
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.
// Otherwise the code should be kept identical
@Suppress("LongParameterList")
private fun interceptFxaRequest(
engineSession: EngineSession,
uri: String,

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

@ -6,19 +6,15 @@ package org.mozilla.fenix.extensions
import android.content.Context
import mozilla.components.concept.engine.EngineSession
import org.json.JSONObject
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.experiments.nimbus.HardcodedNimbusFeatures
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gecko.GeckoProvider
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 {
private lateinit var context: Context
@ -27,49 +23,12 @@ class ExtensionProcessTest {
@Before
fun setUp() {
context = TestHelper.appContext
policy =
context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
policy = context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
}
@Test
fun test_extension_process_can_be_enabled_by_nimbus() {
val hardcodedNimbus = HardcodedNimbusFeatures(
context,
"extensions-process" to JSONObject(
"""
{
"enabled": true
}
""".trimIndent(),
),
)
hardcodedNimbus.connectWith(FxNimbus)
fun test_extension_process_is_enabled() {
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertTrue(FxNimbus.features.extensionsProcess.value().enabled)
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 the test in its testBlock.
* 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) {
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.
*/

@ -22,6 +22,8 @@ import org.junit.Assert
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.utils.IntentUtils
import java.time.LocalDate
import java.time.LocalTime
object DataGenerationHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
@ -75,6 +77,28 @@ object DataGenerationHelper {
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.
*/

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

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

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

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

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

@ -7,7 +7,6 @@ package org.mozilla.fenix.ui
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
@ -265,7 +264,6 @@ class AddressAutofillTest {
}
// 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
fun verifyMultipleAddressesSelectionTest() {
val addressFormPage =

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
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.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -54,6 +55,7 @@ class ComposeNavigationToolbarTest {
@After
fun tearDown() {
mockWebServer.shutdown()
resetSystemLocaleToEnUS()
}
// 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
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Ignore
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
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.TestHelper
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -20,6 +22,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/
class FirefoxSuggestTest {
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule(
@ -33,91 +36,156 @@ class FirefoxSuggestTest {
),
) { 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
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1874831")
@SmokeTest
@Test
fun verifyFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
searchTerm = sponsoredKeyWord,
)
}
}
}
// 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
fun verifyFirefoxSuggestSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amaz")
typeSearch(searchTerm = sponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amaz",
searchTerm = sponsoredKeyWord.dropLast(1),
)
}
}
}
// 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
fun openFirefoxSuggestSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
typeSearch(searchTerm = sponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
)
}.clickSearchSuggestion("Amazon.com - Official Site") {
waitForPageToLoad()
verifyUrl(
"amazon.com/?tag=admarketus-20&ref=pd_sl_924ab4435c5a5c23aa2804307ee0669ab36f88caee841ce51d1f2ecb&mfadid=adm",
searchTerm = sponsoredKeyWord,
)
}.clickSearchSuggestion(sponsoredKeyWords.getValue(sponsoredKeyWord)[0]) {
verifyUrl(sponsoredKeyWords.getValue(sponsoredKeyWord)[1])
verifyTabCounter("1")
}
}
}
// 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
fun verifyFirefoxSuggestSponsoredSearchResultsWithEditedKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Amazon")
deleteSearchKeywordCharacters(numberOfDeletionSteps = 3)
typeSearch(searchTerm = sponsoredKeyWord)
deleteSearchKeywordCharacters(numberOfDeletionSteps = 1)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Amazon.com - Official Site",
sponsoredKeyWords.getValue(sponsoredKeyWord)[0],
"Sponsored",
),
searchTerm = "Amazon",
searchTerm = sponsoredKeyWord,
shouldEditKeyword = true,
numberOfDeletionSteps = 3,
numberOfDeletionSteps = 1,
)
}
}
@ -127,17 +195,17 @@ class FirefoxSuggestTest {
@SmokeTest
@Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marvel")
typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"Firefox Suggest",
"Wikipedia - Marvel Cinematic Universe",
nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[0],
),
searchTerm = "Marvel",
searchTerm = nonSponsoredKeyWord,
)
verifySuggestionsAreNotDisplayed(
rule = activityTestRule,
@ -152,17 +220,17 @@ class FirefoxSuggestTest {
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2348375
@Test
fun verifyFirefoxSuggestNonSponsoredSearchResultsWithPartialKeywordTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marv")
typeSearch(searchTerm = nonSponsoredKeyWord.dropLast(1))
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"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
@Test
fun openFirefoxSuggestNonSponsoredSearchResultsTest() {
AppAndSystemHelper.runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
runWithCondition(TestHelper.appContext.settings().enableFxSuggest) {
navigationToolbar {
}.clickUrlbar {
typeSearch(searchTerm = "Marvel")
typeSearch(searchTerm = nonSponsoredKeyWord)
verifySearchEngineSuggestionResults(
rule = activityTestRule,
searchSuggestions = arrayOf(
"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()
verifyUrl(
"wikipedia.org/wiki/Marvel_Cinematic_Universe",
)
verifyUrl(nonSponsoredKeyWords.getValue(nonSponsoredKeyWord)[1])
}
}
}

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

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

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.AppAndSystemHelper
import org.mozilla.fenix.helpers.AppAndSystemHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -50,6 +51,7 @@ class NavigationToolbarTest {
@After
fun tearDown() {
mockWebServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
}
// 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.ext.components
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.denyPermission
import org.mozilla.fenix.helpers.AppAndSystemHelper.grantSystemPermission
@ -92,6 +93,7 @@ class SearchTest {
@After
fun tearDown() {
searchMockServer.shutdown()
AppAndSystemHelper.resetSystemLocaleToEnUS()
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2154189

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

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

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

@ -14,6 +14,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
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.setSystemLocale
import org.mozilla.fenix.helpers.DataGenerationHelper.setTextToClipBoard
@ -64,6 +65,7 @@ class SettingsSearchTest {
@After
fun tearDown() {
mockWebServer.shutdown()
resetSystemLocaleToEnUS()
}
// TestRail link: https://testrail.stage.mozaws.net/index.php?/cases/view/2203333
@ -424,6 +426,10 @@ class SettingsSearchTest {
fun verifyShowSearchSuggestionsToggleTest() {
homeScreen {
}.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 ")
verifySearchEngineSuggestionResults(
activityTestRule,
@ -438,6 +444,10 @@ class SettingsSearchTest {
}.goBack {
}.goBack {
}.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")
verifySuggestionsAreNotDisplayed(activityTestRule, "mozilla firefox")
}

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

@ -17,6 +17,7 @@ import androidx.compose.ui.test.performClick
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
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.contrib.PickerActions
import androidx.test.espresso.matcher.RootMatchers.isDialog
@ -40,7 +41,6 @@ import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.mozilla.fenix.R
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.TAG
import org.mozilla.fenix.helpers.DataGenerationHelper.getStringResource
@ -686,24 +686,29 @@ class BrowserRobot {
fun verifyCookieBannerExists(exists: Boolean) {
for (i in 1..RETRY_COUNT) {
Log.i(TAG, "verifyCookieBannerExists: For loop: $i")
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
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
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() {
assertUIObjectExists(
itemWithResId("$packageName:id/parentPanel"),
@ -841,7 +846,7 @@ class BrowserRobot {
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() =
assertUIObjectExists(
@ -1149,6 +1154,7 @@ class BrowserRobot {
"$packageName:id/action",
getStringResource(R.string.open_in_app_cfr_positive_button_text),
).clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Clicked \"Go to settings\" open links in apps CFR button")
SettingsRobot().interact()
return SettingsRobot.Transition()
@ -1302,8 +1308,6 @@ fun clearTextFieldItem(item: UiObject) {
item.clearTextField()
}
private fun cookieBanner() = itemWithResId("startsiden-gdpr-disclaimer")
// Context menu items
// Link URL
private fun contextMenuLinkUrl(linkUrl: String) =

@ -26,6 +26,7 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
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.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches
@ -51,7 +52,6 @@ import org.junit.Assert
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
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.DataGenerationHelper.getStringResource
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" +
" else who uses this device."
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar)
fun verifyNavigationToolbar() = assertUIObjectExists(navigationToolbar())
fun verifyHomeScreen() = assertUIObjectExists(homeScreen)
fun verifyHomeScreen() = assertUIObjectExists(homeScreen())
fun verifyPrivateBrowsingHomeScreenItems() {
verifyHomeScreenAppBarItems()
@ -97,19 +97,19 @@ class HomeScreenRobot {
}
fun verifyHomeScreenAppBarItems() =
assertUIObjectExists(homeScreen, privateBrowsingButton, homepageWordmark)
assertUIObjectExists(homeScreen(), privateBrowsingButton(), homepageWordmark())
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 verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() {
homeScreenList().scrollToBeginning(3)
assertUIObjectExists(homepageWordmark)
assertUIObjectExists(homepageWordmark())
}
fun verifyHomeComponent() = assertHomeComponent()
@ -140,7 +140,7 @@ class HomeScreenRobot {
).assertExists()
it.onNodeWithText(
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_2),
getStringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
).assertExists()
it.onNodeWithText(
@ -292,7 +292,7 @@ class HomeScreenRobot {
mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
}
fun clickFirefoxLogo() = homepageWordmark.click()
fun clickFirefoxLogo() = homepageWordmark().click()
fun verifyThoughtProvokingStories(enabled: Boolean) {
if (enabled) {
@ -481,8 +481,8 @@ class HomeScreenRobot {
}
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar.waitForExists(waitingTime)
navigationToolbar.click()
navigationToolbar().waitForExists(waitingTime)
navigationToolbar().click()
mDevice.waitForIdle()
SearchRobot().interact()
@ -502,14 +502,14 @@ class HomeScreenRobot {
fun togglePrivateBrowsingMode(switchPBModeOn: Boolean = true) {
// Switch to private browsing homescreen
if (switchPBModeOn && !isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime)
privateBrowsingButton.click()
privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton().click()
}
// Switch to normal browsing homescreen
if (!switchPBModeOn && isPrivateModeEnabled()) {
privateBrowsingButton.waitForExists(waitingTime)
privateBrowsingButton.click()
privateBrowsingButton().waitForExists(waitingTime)
privateBrowsingButton().click()
}
}
@ -521,7 +521,7 @@ class HomeScreenRobot {
waitingTime,
)
privateBrowsingButton.click()
privateBrowsingButton().click()
}
AddToHomeScreenRobot().interact()
@ -535,7 +535,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar.click()
navigationToolbar().click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -557,7 +557,8 @@ class HomeScreenRobot {
}
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()
return Transition()
@ -631,8 +632,10 @@ class HomeScreenRobot {
}
fun clickSponsoredShortcutsSettingsButton(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Looking for: ${sponsoredShortcutsSettingsButton.selector}")
sponsoredShortcutsSettingsButton.waitForExists(waitingTime)
sponsoredShortcutsSettingsButton.clickAndWaitForNewWindow(waitingTime)
Log.i(TAG, "clickSponsoredShortcutsSettingsButton: Clicked ${sponsoredShortcutsSettingsButton.selector} and waiting for $waitingTime for a new window")
SettingsSubMenuHomepageRobot().interact()
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 sponsoredShortcut(sponsoredShortcutTitle: String) =
mDevice.findObject(
By
.res("$packageName:id/top_site_title")
.textContains(sponsoredShortcutTitle),
onView(
allOf(
withId(R.id.top_site_title),
withText(sponsoredShortcutTitle),
),
)
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private val homeScreen =
private fun homeScreen() =
itemWithResId("$packageName:id/homeLayout")
private val privateBrowsingButton =
private fun privateBrowsingButton() =
itemWithResId("$packageName:id/privateBrowsingButton")
private fun isPrivateModeEnabled(): Boolean =
@ -959,10 +963,10 @@ private fun isPrivateModeEnabled(): Boolean =
"Disable private browsing",
).exists()
private val homepageWordmark =
private fun homepageWordmark() =
itemWithResId("$packageName:id/wordmark")
private val navigationToolbar =
private fun navigationToolbar() =
itemWithResId("$packageName:id/toolbar")
private val menuButton =
itemWithResId("$packageName:id/menuButton")

@ -15,6 +15,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
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.matches
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.itemWithResIdAndText
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.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -149,7 +149,7 @@ class NavigationToolbarRobot {
assertTrue(
itemWithResId("$packageName:id/browserLayout").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()
}
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 {
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.")
NavigationToolbarRobot().interact()
@ -388,8 +398,7 @@ private fun awesomeBar() =
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 tabTrayButton() = onView(withId(R.id.tab_button))
private fun tabsCounter() =
mDevice.findObject(By.res("$packageName:id/counter_root"))
private fun tabsCounter() = onView(withId(R.id.mozac_browser_toolbar_browser_actions))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
private fun readerViewToggle() =

@ -107,6 +107,27 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading()
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 verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
@ -583,6 +604,7 @@ private fun assertOpenLinksInAppsButton() {
scrollToElementByText("Open links in apps")
openLinksInAppsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
Log.i(TAG, "clickOpenLinksInAppsGoToSettingsCFRButton: Verified \"Open links in apps\" setting option")
}
// ADVANCED SECTION

@ -124,7 +124,8 @@ private fun assertCurrentTimestamp() {
private fun assertWhatIsNewInFirefoxPreview() {
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)))
.perform(click())
}

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

@ -118,7 +118,7 @@ class SettingsSubMenuLoginsAndPasswordsSavedLoginsRobot {
)
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) =
mDevice.waitNotNull(Until.findObjects(By.text(username)))

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

@ -354,6 +354,7 @@
<service
android:name=".downloads.DownloadService"
android:foregroundServiceType="dataSync"
android:exported="false" />
<receiver
@ -374,8 +375,14 @@
</intent-filter>
</receiver>
<service android:name=".session.PrivateNotificationService"
android:exported="false" />
<service
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
android:name=".messaging.NotificationDismissedService"

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

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

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

@ -29,14 +29,12 @@ import androidx.annotation.CallSuper
import androidx.annotation.IdRes
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
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.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonsManagementFragmentDirections
import org.mozilla.fenix.addons.ExtensionsProcessDisabledController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledBackgroundController
import org.mozilla.fenix.addons.ExtensionsProcessDisabledForegroundController
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
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.databinding.ActivityHomeBinding
import org.mozilla.fenix.debugsettings.data.DefaultDebugSettingsRepository
import org.mozilla.fenix.debugsettings.ui.DebugOverlay
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.debugsettings.ui.FenixOverlay
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
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.nav
import org.mozilla.fenix.ext.setNavigationIcon
import org.mozilla.fenix.ext.settings
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.CrashReporterIntentProcessor
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.SpeechProcessingIntentProcessor
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.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.FenixNimbusMessagingController
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.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
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.tabhistory.TabHistoryDialogFragment
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
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.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
import java.util.Locale
@ -176,7 +153,7 @@ import java.util.Locale
* - home screen
* - browser screen
*/
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList", "LongMethod")
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongMethod")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// 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 {
ExtensionsProcessDisabledController(this@HomeActivity)
private val extensionsProcessDisabledForegroundController by lazy {
ExtensionsProcessDisabledForegroundController(this@HomeActivity)
}
private val extensionsProcessDisabledBackgroundController by lazy {
ExtensionsProcessDisabledBackgroundController(
browserStore = components.core.store,
appStore = components.appStore,
)
}
private val serviceWorkerSupport by lazy {
@ -300,9 +284,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
visibility = View.VISIBLE
setContent {
FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) {
DebugOverlay()
}
FenixOverlay(
browserStore = components.core.store,
inactiveTabsEnabled = settings().inactiveTabsAreEnabled,
)
}
} else {
setContent {}
@ -348,7 +333,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// Unless the activity is recreated, navigate to home first (without rendering it)
// to add it to the back stack.
if (savedInstanceState == null) {
navigateToHome()
navigateToHome(navHost.navController)
}
if (!shouldStartOnHome() && shouldNavigateToBrowserOnColdStart(savedInstanceState)) {
@ -392,7 +377,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
lifecycle.addObservers(
webExtensionPopupObserver,
extensionsProcessDisabledPromptObserver,
extensionsProcessDisabledForegroundController,
extensionsProcessDisabledBackgroundController,
serviceWorkerSupport,
webExtensionPromptFeature,
)
@ -699,7 +685,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
@ -886,20 +877,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
* 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
* value of [searchTermOrURL]).
@ -1003,7 +978,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
* was opened from history.
* @param additionalHeaders The extra headers to use when loading the URL.
*/
@Suppress("LongParameterList")
fun openToBrowserAndLoad(
searchTermOrURL: String,
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]).
*
@ -1194,7 +1109,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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
// 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) {
@ -1203,8 +1123,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
open fun navigateToHome() {
navHost.navController.navigate(NavGraphDirections.actionStartupHome())
@VisibleForTesting
internal fun navigateToHome(navController: NavController) {
if (this is ExternalAppBrowserActivity) {
return
}
navController.navigate(NavGraphDirections.actionStartupHome())
}
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
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.widget.Button
import android.widget.TextView
@ -20,35 +18,30 @@ import mozilla.components.support.webextensions.ExtensionsProcessDisabledPromptO
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.ext.components
import kotlin.system.exitProcess
/**
* 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
* process spawning or disable extensions). When in background, we kill the app to prevent
* extensions from being disabled and network requests continuing.
* process spawning or disable extensions).
*
* @param context to show the AlertDialog
* @param browserStore The [BrowserStore] which holds the state for showing the dialog
* @param appStore The [AppStore] containing the application state
* @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 onKillApp called when the app is backgrounded and extensions process is disabled
*/
class ExtensionsProcessDisabledController(
class ExtensionsProcessDisabledForegroundController(
@UiContext context: Context,
browserStore: BrowserStore = context.components.core.store,
appStore: AppStore = context.components.appStore,
builder: AlertDialog.Builder = AlertDialog.Builder(context),
appName: String = context.appName,
onKillApp: () -> Unit = { killApp() },
) : ExtensionsProcessDisabledPromptObserver(
browserStore,
store = browserStore,
shouldCancelOnStop = true,
{
if (appStore.state.isForeground) {
presentDialog(context, browserStore, builder, appName)
} else {
onKillApp.invoke()
}
},
) {
@ -61,16 +54,6 @@ class ExtensionsProcessDisabledController(
companion object {
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
* 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.Bundle
import android.provider.Settings
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
@ -31,9 +32,11 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
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.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.concept.storage.LoginEntry
import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature
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.DialogColorsProvider
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.readerview.ReaderViewFeature
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.sync.autofill.DefaultCreditCardValidationDelegate
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.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler
@ -108,6 +115,7 @@ import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.MediaState
import org.mozilla.fenix.GleanMetrics.PullToRefreshInBrowser
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.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference
import kotlin.coroutines.cancellation.CancellationException
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/**
@ -460,6 +469,7 @@ abstract class BaseBrowserFragment :
browserToolbarView.view.display.setOnSiteSecurityClickedListener {
showQuickSettingsDialog()
Events.browserToolbarSecurityIndicatorTapped.record()
}
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 {
override val creditCardPickerView
get() = binding.creditCardSelectBar
@ -894,9 +916,12 @@ abstract class BaseBrowserFragment :
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
if (binding.swipeRefresh.isEnabled) {
val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.textPrimary, context)
binding.swipeRefresh.setColorSchemeColors(primaryTextColor)
val primaryTextColor = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
val primaryBackgroundColor = ThemeManager.resolveAttribute(R.attr.layer2, context)
binding.swipeRefresh.apply {
setColorSchemeResources(primaryTextColor)
setProgressBackgroundColorSchemeResource(primaryBackgroundColor)
}
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
requireComponents.core.store,
@ -1651,4 +1676,38 @@ abstract class BaseBrowserFragment :
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 actionToPerform The action to be performed on action button press.
*/
@Suppress("LongParameterList")
class DynamicInfoBanner(
private val context: Context,
container: ViewGroup,

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

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

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

@ -406,6 +406,14 @@ class DefaultBrowserToolbarMenuController(
.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) {
when (item) {
is ToolbarMenu.Item.OpenInFenix ->
@ -433,10 +441,19 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("open_in_app"))
is ToolbarMenu.Item.CustomizeReaderView ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reader_mode_appearance"))
is ToolbarMenu.Item.Back ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back"))
is ToolbarMenu.Item.Back -> {
if (item.viewHistory) {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back_long_press"))
} else {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("back"))
}
}
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 ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("reload"))
is ToolbarMenu.Item.Stop ->
@ -483,6 +500,12 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("set_default_browser"))
is ToolbarMenu.Item.RemoveFromTopSites ->
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.nimbus.FxNimbus
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.
@ -55,7 +54,7 @@ import org.mozilla.fenix.utils.Settings
* @param pinnedSiteStorage Used to check if the current url is a pinned site.
* @property isPinningSupported true if the launcher supports adding shortcuts.
*/
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions")
open class DefaultToolbarMenu(
private val context: Context,
private val store: BrowserStore,
@ -193,6 +192,14 @@ open class DefaultToolbarMenu(
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active
} ?: false
/**
* Should Translations menu item be visible?
*/
@VisibleForTesting(otherwise = PRIVATE)
fun shouldShowTranslations(): Boolean = selectedSession?.let {
context.settings().enableTranslations
} ?: false
// End of predicates //
private val installToHomescreen = BrowserMenuHighlightableItem(
@ -248,6 +255,14 @@ open class DefaultToolbarMenu(
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(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
@ -405,6 +420,7 @@ open class DefaultToolbarMenu(
syncMenuItem(),
BrowserMenuDivider(),
findInPageItem,
translationsItem.apply { visible = ::shouldShowTranslations },
desktopSiteItem,
openInRegularTabItem.apply { visible = ::shouldShowOpenInRegularTab },
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },

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

@ -18,6 +18,11 @@ interface ToolbarMenu {
*/
object OpenInRegularTab : Item()
object FindInPage : Item()
/**
* Opens the translations flow.
*/
object Translate : Item()
object Share : Item()
data class Back(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].
*/
@Composable
@Suppress("LongParameterList")
fun Image(
url: String,
modifier: Modifier = Modifier,

@ -7,10 +7,22 @@ package org.mozilla.fenix.compose
import androidx.annotation.VisibleForTesting
import androidx.compose.foundation.background
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.material.Card
import androidx.compose.material.Text
import androidx.compose.material.TextButton
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.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.semantics
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.TextDecoration
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
/**
@ -67,6 +82,12 @@ fun LinkText(
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
// 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
@ -76,12 +97,17 @@ fun LinkText(
style = style,
modifier = Modifier.semantics(mergeDescendants = true) {
onClick {
linkTextStates.firstOrNull()?.let {
it.onClick(it.url)
if (linkTextStates.size > 1) {
showDialog.value = true
} else {
linkTextStates.firstOrNull()?.let {
it.onClick(it.url)
}
}
return@onClick true
}
contentDescription = "$annotatedString $linksAvailable"
},
onClick = { charOffset ->
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
internal fun onTextClick(
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.
*
* @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 onCheckedChange Invoked when Switch is being clicked, therefore the change of checked
* state is requested.
@ -44,6 +45,7 @@ private const val DISABLED_ALPHA = 0.5f
@Composable
fun SwitchWithLabel(
label: String,
description: String? = null,
checked: Boolean,
onCheckedChange: ((Boolean) -> Unit),
modifier: Modifier = Modifier,
@ -60,16 +62,28 @@ fun SwitchWithLabel(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
color = if (enabled) {
FirefoxTheme.colors.textPrimary
} else {
FirefoxTheme.colors.textDisabled
},
style = FirefoxTheme.typography.subtitle1,
modifier = Modifier.weight(1f),
)
Column(
modifier = Modifier
.weight(1f),
) {
Text(
text = label,
color = if (enabled) {
FirefoxTheme.colors.textPrimary
} else {
FirefoxTheme.colors.textDisabled
},
style = FirefoxTheme.typography.subtitle1,
)
description?.let {
Text(
text = description,
color = FirefoxTheme.colors.textSecondary,
style = FirefoxTheme.typography.body2,
)
}
}
Switch(
modifier = Modifier.clearAndSetSemantics {},
@ -139,6 +153,7 @@ private fun SwitchWithLabelPreview() {
var enabledSwitchState by remember { mutableStateOf(false) }
SwitchWithLabel(
label = if (enabledSwitchState) "On" else "Off",
description = "Description text",
checked = enabledSwitchState,
onCheckedChange = { enabledSwitchState = it },
)

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

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

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

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

@ -5,24 +5,16 @@
package org.mozilla.fenix.customtabs
import android.app.assist.AssistContent
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.browser.state.selector.findCustomTab
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 org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.NavGraphDirections
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"
@ -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() {
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.WebAppHideToolbarFeature
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.ktx.android.arch.lifecycle.addObservers
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.
*/
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
class ExternalAppBrowserFragment : BaseBrowserFragment() {
private val args by navArgs<ExternalAppBrowserFragmentArgs>()
@ -212,9 +211,4 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
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
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Text
import androidx.compose.material.rememberDrawerState
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.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.LayoutDirection
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.compose.annotation.LightDarkPreview
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
/**
* 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
fun DebugOverlay() {
fun DebugOverlay(
navController: NavHostController,
drawerStatus: DrawerStatus,
debugDrawerDestinations: List<DebugDrawerDestination>,
onDrawerOpen: () -> Unit,
onDrawerClose: () -> Unit,
onDrawerBackButtonClick: () -> Unit,
) {
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(
modifier = Modifier.fillMaxSize(),
@ -41,12 +86,41 @@ fun DebugOverlay() {
.align(Alignment.CenterStart)
.padding(start = 16.dp),
onClick = {
scope.launch {
snackbarState.showSnackbar("Show debug drawer")
}
onDrawerOpen()
},
)
// 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
SnackbarHost(
hostState = snackbarState,
@ -62,13 +136,41 @@ fun DebugOverlay() {
@Composable
@LightDarkPreview
private fun DebugOverlayPreview() {
FirefoxTheme {
Box(
modifier = Modifier
.fillMaxSize()
.background(color = FirefoxTheme.colors.layer1),
) {
DebugOverlay()
val navController = rememberNavController()
var drawerStatus by remember { mutableStateOf(DrawerStatus.Closed) }
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 {
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 = {
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) {

@ -15,12 +15,47 @@ import androidx.annotation.DrawableRes
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
import mozilla.components.concept.base.crash.Breadcrumb
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.HomeActivity
import org.mozilla.fenix.NavGraphDirections
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.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.
@ -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 SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
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.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
import org.mozilla.geckoview.GeckoRuntime
@ -134,7 +133,7 @@ object GeckoProvider {
.consoleOutput(context.components.settings.enableGeckoLogs)
.debugLogging(Config.channel.isDebug || context.components.settings.enableGeckoLogs)
.aboutConfigEnabled(true)
.extensionsProcessEnabled(FxNimbus.features.extensionsProcess.value().enabled)
.extensionsProcessEnabled(true)
.extensionsWebAPIEnabled(true)
.build()
}

@ -49,7 +49,9 @@ import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import mozilla.components.browser.state.selector.findTab
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.nimbus.FxNimbus
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.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
@ -1008,17 +1009,18 @@ class HomeFragment : Fragment() {
lastAppliedWallpaperName = wallpaperName
}
else -> {
runBlockingIncrement {
viewLifecycleOwner.lifecycleScope.launch {
// loadBitmap does file lookups based on name, so we don't need a fully
// qualified type to load the image
val wallpaper = Wallpaper.Default.copy(name = wallpaperName)
val wallpaperImage =
requireComponents.useCases.wallpaperUseCases.loadBitmap(wallpaper)
context?.let { requireComponents.useCases.wallpaperUseCases.loadBitmap(it, wallpaper) }
wallpaperImage?.let {
it.scaleToBottomOfView(binding.wallpaperImageView)
binding.wallpaperImageView.isVisible = true
lastAppliedWallpaperName = wallpaperName
} ?: run {
if (!isActive) return@run
with(binding.wallpaperImageView) {
isVisible = false
showSnackBar(
@ -1052,11 +1054,15 @@ class HomeFragment : Fragment() {
}
private fun observeWallpaperUpdates() {
consumeFrom(requireComponents.appStore) {
val currentWallpaper = it.wallpaperState.currentWallpaper
if (currentWallpaper.name != lastAppliedWallpaperName) {
applyWallpaper(wallpaperName = currentWallpaper.name, orientationChange = false)
}
consumeFlow(requireComponents.appStore, viewLifecycleOwner) { flow ->
flow.filter { it.mode == BrowsingMode.Normal }
.map { it.wallpaperState.currentWallpaper }
.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.
* @param fxaEntrypoint The source entry point to FxA.
*/
@Suppress("LongParameterList")
class HomeMenuView(
private val view: View,
private val context: Context,

@ -239,7 +239,7 @@ fun PocketSponsoredStory(
* @param onDiscoverMoreClicked Callback for when the user taps an element which contains an
*/
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList", "LongMethod")
@Suppress("LongMethod")
@Composable
fun PocketStories(
@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.
*/
@OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList")
@Composable
fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>,

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.recentbookmarks.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
@ -42,10 +41,10 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ContextualMenu
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.MenuItem
import org.mozilla.fenix.compose.annotation.LightDarkPreview
@ -172,6 +171,11 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
modifier = imageModifier,
targetSize = imageWidth,
contentScale = ContentScale.Crop,
fallback = {
if (!bookmark.url.isNullOrEmpty()) {
FallbackBookmarkFaviconImage(url = bookmark.url)
}
},
)
}
!bookmark.url.isNullOrEmpty() && !inComposePreview -> {
@ -180,23 +184,7 @@ private fun RecentBookmarkImage(bookmark: RecentBookmark) {
PlaceholderBookmarkImage()
}
WithIcon { icon ->
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,
)
}
}
FallbackBookmarkFaviconImage(bookmark.url)
}
}
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
@LightDarkPreview
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.
*/
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongMethod", "LongParameterList")
@Suppress("LongMethod")
@Composable
fun RecentSyncedTab(
tab: RecentSyncedTab?,

@ -240,6 +240,15 @@ fun RecentTabImage(
modifier = modifier,
targetSize = THUMBNAIL_SIZE.dp,
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(

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

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

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

@ -4,26 +4,26 @@
package org.mozilla.fenix.messaging
import android.app.Activity
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
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.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import kotlinx.coroutines.launch
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.Message
import mozilla.components.support.base.ids.SharedIdsHelper
import mozilla.components.support.utils.BootUtils
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.createBaseNotification
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
* [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() {
/**
* This service cannot be bound to.
*/
override fun onBind(intent: Intent?): IBinder? = null
class NotificationDismissedService : LifecycleService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (intent != null) {
val nimbusMessagingController =
FenixNimbusMessagingController(applicationContext.components.analytics.messagingStorage)
// Get the relevant message.
val message = intent.getStringExtra(DISMISSED_MESSAGE_ID)?.let { messageId ->
runBlockingIncrement { nimbusMessagingController.getMessage(messageId) }
}
lifecycleScope.launch {
// Get the relevant message.
val message = intent.getStringExtra(DISMISSED_MESSAGE_ID)?.let { messageId ->
nimbusMessagingController.getMessage(messageId)
}
if (message != null) {
// Update message as 'dismissed'.
runBlockingIncrement { nimbusMessagingController.onMessageDismissed(message.metadata) }
if (message != null) {
// Update message as 'dismissed'.
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
* [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?) {
super.onCreate(savedInstanceState)
@ -213,22 +212,24 @@ class NotificationClickedReceiverActivity : Activity() {
val nimbusMessagingController =
FenixNimbusMessagingController(components.analytics.messagingStorage)
// Get the relevant message.
val message = intent.getStringExtra(CLICKED_MESSAGE_ID)?.let { messageId ->
runBlockingIncrement { nimbusMessagingController.getMessage(messageId) }
}
lifecycleScope.launch {
// Get the relevant message.
val message = intent.getStringExtra(CLICKED_MESSAGE_ID)?.let { messageId ->
nimbusMessagingController.getMessage(messageId)
}
if (message != null) {
// Update message as 'clicked'.
runBlockingIncrement { nimbusMessagingController.onMessageClicked(message.metadata) }
if (message != null) {
// Update message as 'clicked'.
nimbusMessagingController.onMessageClicked(message.metadata)
// Create the intent.
val intent = nimbusMessagingController.getIntentForMessage(message)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// Create the intent.
val intent = nimbusMessagingController.getIntentForMessage(message)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
// Start the message intent.
startActivity(intent)
// Start the message intent.
startActivity(intent)
}
}
// End this activity.

@ -18,21 +18,23 @@ import android.view.ViewGroup
import androidx.annotation.RequiresApi
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.NotificationManagerCompat
import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.navigation.fragment.findNavController
import mozilla.components.service.nimbus.evalJexlSafe
import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import mozilla.components.support.utils.BrowsersCache
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.requireComponents
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.OnboardingScreen
import org.mozilla.fenix.onboarding.view.sequencePosition
@ -49,6 +51,7 @@ class OnboardingFragment : Fragment() {
private val pagesToDisplay by lazy {
pagesToDisplay(
shouldShowDefaultBrowserCard(requireContext()),
canShowNotificationPage(requireContext()),
canShowAddWidgetCard(),
)
@ -59,6 +62,11 @@ class OnboardingFragment : Fragment() {
@SuppressLint("SourceLockedOrientationActivity")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (pagesToDisplay.isEmpty()) {
/* do not continue if there's no onboarding pages to display */
onFinish(null)
}
if (isNotATablet()) {
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
@ -97,7 +105,6 @@ class OnboardingFragment : Fragment() {
@Composable
@Suppress("LongMethod")
private fun ScreenContent() {
val context = LocalContext.current
OnboardingScreen(
pagesToDisplay = pagesToDisplay,
onMakeFirefoxDefaultClick = {
@ -113,18 +120,6 @@ class OnboardingFragment : Fragment() {
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 = {
findNavController().nav(
id = R.id.onboardingFragment,
@ -172,10 +167,7 @@ class OnboardingFragment : Fragment() {
)
},
onFinish = {
onFinish(
sequenceId = pagesToDisplay.telemetrySequenceId(),
sequencePosition = pagesToDisplay.sequencePosition(it.type),
)
onFinish(it)
},
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()
findNavController().nav(
id = R.id.onboardingFragment,
directions = OnboardingFragmentDirections.actionHome(),
)
telemetryRecorder.onOnboardingComplete(
sequenceId = sequenceId,
sequencePosition = sequencePosition,
)
}
private fun shouldShowDefaultBrowserCard(context: Context) =
!BrowsersCache.all(context.applicationContext).isDefaultBrowser
private fun canShowNotificationPage(context: Context) =
!NotificationManagerCompat.from(context.applicationContext)
.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 pagesToDisplay(
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean,
showAddWidgetPage: Boolean,
): List<OnboardingPageUiData> {
val jexlConditions = FxNimbus.features.junoOnboarding.value().conditions
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(
privacyCaption,
showDefaultBrowserPage,
showNotificationPage,
showAddWidgetPage,
jexlConditions,

@ -4,15 +4,15 @@
package org.mozilla.fenix.onboarding.view
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.nimbus.OnboardingCardData
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].
*/
internal fun Collection<OnboardingCardData>.toPageUiData(
privacyCaption: Caption,
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean,
showAddWidgetPage: Boolean,
jexlConditions: Map<String, String>,
@ -21,16 +21,25 @@ internal fun Collection<OnboardingCardData>.toPageUiData(
// we are first filtering the cards based on Nimbus configuration
return filter { it.shouldDisplayCard(func, jexlConditions) }
// we are then filtering again based on device capabilities
.filter { it.isCardEnabled(showNotificationPage, showAddWidgetPage) }
.filter { it.isCardEnabled(showDefaultBrowserPage, showNotificationPage, showAddWidgetPage) }
.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(
showDefaultBrowserPage: Boolean,
showNotificationPage: Boolean,
showAddWidgetPage: Boolean,
): Boolean =
when (cardType) {
OnboardingCardType.DEFAULT_BROWSER -> {
enabled && showDefaultBrowserPage
}
OnboardingCardType.NOTIFICATION_PERMISSION -> {
enabled && showNotificationPage
}
@ -91,14 +100,14 @@ private fun OnboardingCardData.shouldDisplayCard(
return validPrerequisites && !hasDisqualifiers
}
private fun OnboardingCardData.toPageUiData() = OnboardingPageUiData(
private fun OnboardingCardData.toPageUiData(privacyCaption: Caption?) = OnboardingPageUiData(
type = cardType.toPageUiDataType(),
imageRes = imageRes.resourceId,
title = title,
description = body,
linkText = linkText,
primaryButtonLabel = primaryButtonLabel,
secondaryButtonLabel = secondaryButtonLabel,
privacyCaption = privacyCaption,
)
private fun OnboardingCardType.toPageUiDataType() = when (this) {
@ -117,7 +126,6 @@ internal fun mapToOnboardingPageState(
onboardingPageUiData: OnboardingPageUiData,
onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onSignInButtonClick: () -> Unit,
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
@ -129,14 +137,12 @@ internal fun mapToOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onMakeFirefoxDefaultClick,
onNegativeButtonClick = onMakeFirefoxDefaultSkipClick,
onUrlClick = onPrivacyPolicyClick,
)
OnboardingPageUiData.Type.ADD_SEARCH_WIDGET -> createOnboardingPageState(
onboardingPageUiData = onboardingPageUiData,
onPositiveButtonClick = onAddFirefoxWidgetClick,
onNegativeButtonClick = onAddFirefoxWidgetSkipClick,
onUrlClick = onPrivacyPolicyClick,
)
OnboardingPageUiData.Type.SYNC_SIGN_IN -> createOnboardingPageState(
@ -156,18 +162,11 @@ private fun createOnboardingPageState(
onboardingPageUiData: OnboardingPageUiData,
onPositiveButtonClick: () -> Unit,
onNegativeButtonClick: () -> Unit,
onUrlClick: (String) -> Unit = {},
): OnboardingPageState = OnboardingPageState(
imageRes = onboardingPageUiData.imageRes,
title = onboardingPageUiData.title,
description = onboardingPageUiData.description,
linkTextState = onboardingPageUiData.linkText?.let {
LinkTextState(
text = it,
url = SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
onClick = onUrlClick,
)
},
primaryButton = Action(onboardingPageUiData.primaryButtonLabel, onPositiveButtonClick),
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 org.mozilla.fenix.R
import org.mozilla.fenix.compose.LinkText
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.compose.button.PrimaryButton
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.
*/
@Composable
@Suppress("LongMethod")
fun OnboardingPage(
pageState: OnboardingPageState,
modifier: Modifier = Modifier,
@ -116,10 +116,21 @@ fun OnboardingPage(
Spacer(modifier = Modifier.height(16.dp))
DescriptionText(
description = pageState.description,
linkTextState = pageState.linkTextState,
Text(
text = pageState.description,
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(
@ -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.
*/

@ -13,7 +13,7 @@ import org.mozilla.fenix.compose.LinkTextState
* @property imageRes [DrawableRes] displayed on the page.
* @property title [String] title 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 secondaryButton [Action] action for the secondary button.
* @property onRecordImpressionEvent Callback for recording impression event.
@ -22,7 +22,7 @@ data class OnboardingPageState(
@DrawableRes val imageRes: Int,
val title: String,
val description: String,
val linkTextState: LinkTextState? = null,
val privacyCaption: Caption? = null,
val primaryButton: Action,
val secondaryButton: Action? = null,
val onRecordImpressionEvent: () -> Unit = {},
@ -35,3 +35,11 @@ data class Action(
val text: String,
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,
val title: String,
val description: String,
val linkText: String? = null,
val primaryButtonLabel: String,
val secondaryButtonLabel: String,
val privacyCaption: Caption?,
) {
/**
* Model for different types of Onboarding Pages.

@ -37,6 +37,7 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.state.ext.observeAsComposableState
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.LinkTextState
import org.mozilla.fenix.compose.PagerIndicator
import org.mozilla.fenix.compose.annotation.LightDarkPreview
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 onMakeFirefoxDefaultClick Invoked when positive 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 onSkipSignInClick Invoked when the negative button on the sign in page is clicked.
* @param onNotificationPermissionButtonClick Invoked when positive button on notification page is
@ -65,7 +65,6 @@ fun OnboardingScreen(
pagesToDisplay: List<OnboardingPageUiData>,
onMakeFirefoxDefaultClick: () -> Unit,
onSkipDefaultClick: () -> Unit,
onPrivacyPolicyClick: (url: String) -> Unit,
onSignInButtonClick: () -> Unit,
onSkipSignInClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
@ -127,9 +126,6 @@ fun OnboardingScreen(
scrollToNextPageOrDismiss()
onSkipDefaultClick()
},
onPrivacyPolicyClick = {
onPrivacyPolicyClick(it)
},
onSignInButtonClick = {
onSignInButtonClick()
scrollToNextPageOrDismiss()
@ -167,7 +163,6 @@ private fun OnboardingContent(
pagerState: PagerState,
onMakeFirefoxDefaultClick: () -> Unit,
onMakeFirefoxDefaultSkipClick: () -> Unit,
onPrivacyPolicyClick: (String) -> Unit,
onSignInButtonClick: () -> Unit,
onSignInSkipClick: () -> Unit,
onNotificationPermissionButtonClick: () -> Unit,
@ -195,7 +190,6 @@ private fun OnboardingContent(
onboardingPageUiData = pageUiState,
onMakeFirefoxDefaultClick = onMakeFirefoxDefaultClick,
onMakeFirefoxDefaultSkipClick = onMakeFirefoxDefaultSkipClick,
onPrivacyPolicyClick = onPrivacyPolicyClick,
onSignInButtonClick = onSignInButtonClick,
onSignInSkipClick = onSignInSkipClick,
onNotificationPermissionButtonClick = onNotificationPermissionButtonClick,
@ -251,7 +245,6 @@ private fun OnboardingScreenPreview() {
},
onMakeFirefoxDefaultClick = {},
onMakeFirefoxDefaultSkipClick = {},
onPrivacyPolicyClick = {},
onSignInButtonClick = {},
onSignInSkipClick = {},
onNotificationPermissionButtonClick = {},
@ -268,10 +261,17 @@ private fun defaultPreviewPages() = listOf(
type = OnboardingPageUiData.Type.DEFAULT_BROWSER,
imageRes = R.drawable.ic_onboarding_welcome,
title = stringResource(R.string.juno_onboarding_default_browser_title_nimbus_2),
description = stringResource(R.string.juno_onboarding_default_browser_description_nimbus_2),
linkText = stringResource(R.string.juno_onboarding_default_browser_description_link_text),
description = stringResource(R.string.juno_onboarding_default_browser_description_nimbus_3),
primaryButtonLabel = stringResource(R.string.juno_onboarding_default_browser_positive_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(
type = OnboardingPageUiData.Type.SYNC_SIGN_IN,
@ -280,6 +280,14 @@ private fun defaultPreviewPages() = listOf(
description = stringResource(R.string.juno_onboarding_sign_in_description_2),
primaryButtonLabel = stringResource(R.string.juno_onboarding_sign_in_positive_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(
type = OnboardingPageUiData.Type.NOTIFICATION_PERMISSION,
@ -288,5 +296,13 @@ private fun defaultPreviewPages() = listOf(
description = stringResource(R.string.juno_onboarding_enable_notifications_description_nimbus_2),
primaryButtonLabel = stringResource(R.string.juno_onboarding_enable_notifications_positive_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()
}
}
Events.browserToolbarQrScanCompleted.record()
},
)
}

@ -141,7 +141,6 @@ data class SearchFragmentState(
/**
* Creates the initial state for the search fragment.
*/
@Suppress("LongParameterList")
fun createInitialSearchFragmentState(
activity: HomeActivity,
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.res.resolveAttribute
import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.search.SearchEngineSource
@ -116,6 +117,10 @@ class ToolbarView(
url = 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.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import mozilla.components.browser.state.selector.selectedTab
@ -35,17 +36,22 @@ class PrivateNotificationService : AbstractPrivateNotificationService() {
override fun NotificationCompat.Builder.buildNotification() {
setSmallIcon(R.drawable.ic_private_browsing)
setContentTitle(
applicationContext.getString(
R.string.app_name_private_4,
getString(R.string.app_name),
),
)
setContentText(
applicationContext.getString(
R.string.notification_pbm_delete_text_2,
),
)
val contentTitle = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
applicationContext.getString(R.string.notification_erase_title_android_14)
} else {
applicationContext.getString(R.string.app_name_private_4, getString(R.string.app_name))
}
val contentText = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
getString(R.string.notification_erase_text_android_14)
} else {
getString(R.string.notification_pbm_delete_text_2)
}
setContentTitle(contentTitle)
setContentText(contentText)
color = ContextCompat.getColor(
this@PrivateNotificationService,
R.color.pbm_notification_color,

@ -59,6 +59,7 @@ object SupportUtils {
UNSIGNED_ADDONS("unsigned-addons"),
REVIEW_QUALITY_CHECK("review_checker_mobile"),
FX_SUGGEST("search-suggestions-firefox"),
TRANSLATIONS("android-translation"),
}
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
* preference status of "Reconnect".
*/
@Suppress("LongParameterList")
class SyncPreferenceView(
private val syncPreference: SyncPreference,
lifecycleOwner: LifecycleOwner,

@ -136,7 +136,8 @@ class AboutFragment : Fragment(), AboutPageListener {
WHATS_NEW,
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(
AboutItem.ExternalLink(

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

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

Loading…
Cancel
Save