Merge tag 'v109.1.1' into fork

fork
akliuxingyuan 1 year ago
commit 782e8980b0
No known key found for this signature in database
GPG Key ID: 4D182987C034F366

@ -1,4 +1,12 @@
--- ---
cookie-banners:
description: Features for cookie banner handling.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
growth-data: growth-data:
description: A feature measuring campaign growth data description: A feature measuring campaign growth data
hasExposure: true hasExposure: true
@ -63,6 +71,14 @@ nimbus-validation:
settings-title: settings-title:
type: string type: string
description: The title of displayed in the Settings screen and app menu. description: The title of displayed in the Settings screen and app menu.
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the re-engagement notification is shown to the inactive user."
search-term-groups: search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from. description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true hasExposure: true

@ -122,7 +122,7 @@ tasks:
name: "Decision Task for cron job ${cron.job_name}" name: "Decision Task for cron job ${cron.job_name}"
description: 'Created by a [cron task](https://firefox-ci-tc.services.mozilla.com/tasks/${cron.task_id})' description: 'Created by a [cron task](https://firefox-ci-tc.services.mozilla.com/tasks/${cron.task_id})'
provisionerId: "${trustDomain}-${level}" provisionerId: "${trustDomain}-${level}"
workerType: "decision" workerType: "decision-gcp"
tags: tags:
$if: 'tasks_for in ["github-push", "github-pull-request"]' $if: 'tasks_for in ["github-push", "github-pull-request"]'
then: then:

@ -687,6 +687,7 @@ dependencies {
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3' testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation Deps.mozilla_support_rusthttp implementation Deps.mozilla_support_rusthttp
androidTestImplementation Deps.mockk_android
testImplementation Deps.mockk testImplementation Deps.mockk
// For the initial release of Glean 19, we require consumer applications to // For the initial release of Glean 19, we require consumer applications to

@ -206,6 +206,32 @@ events:
- https://github.com/mozilla-mobile/fenix/issues/27779 - https://github.com/mozilla-mobile/fenix/issues/27779
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27780 - https://github.com/mozilla-mobile/fenix/pull/27780
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
re_engagement_notif_tapped:
type: event
description: |
User tapped on the re-engagement notification
bugs:
- https://github.com/mozilla-mobile/fenix/issues/27949
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27978
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 122
re_engagement_notif_shown:
type: event
description: |
Re-engagement notification was shown to the user
bugs:
- https://github.com/mozilla-mobile/fenix/issues/27949
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27978
data_sensitivity: data_sensitivity:
- technical - technical
notification_emails: notification_emails:
@ -1105,6 +1131,7 @@ metrics:
to identify installs from Mozilla Online. to identify installs from Mozilla Online.
send_in_pings: send_in_pings:
- metrics - metrics
- baseline
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/16075 - https://github.com/mozilla-mobile/fenix/issues/16075
data_reviews: data_reviews:
@ -1116,6 +1143,8 @@ metrics:
- android-probes@mozilla.com - android-probes@mozilla.com
- kbrosnan@mozilla.com - kbrosnan@mozilla.com
expires: never expires: never
no_lint:
- BASELINE_PING
metadata: metadata:
tags: tags:
- China - China
@ -1906,12 +1935,14 @@ customize_home:
An indication of whether Contile is enabled to be displayed An indication of whether Contile is enabled to be displayed
send_in_pings: send_in_pings:
- metrics - metrics
- topsites-impression
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/24467 - https://github.com/mozilla-mobile/fenix/issues/24467
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24468 - https://github.com/mozilla-mobile/fenix/pull/24468
data_sensitivity: data_sensitivity:
- interaction - interaction
lifetime: application
notification_emails: notification_emails:
- android-probes@mozilla.com - android-probes@mozilla.com
expires: 112 expires: 112
@ -6200,18 +6231,28 @@ browser.search:
type: labeled_counter type: labeled_counter
description: | description: |
Records counts of SERP pages with adverts displayed. Records counts of SERP pages with adverts displayed.
The key format is `<provider-name>`. The key format is
`<provider-name>.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`,
where:
* `provider-name` is the name of the provider,
* `sap|sap-follow-on|organic` is the search access point,
* `code` is set when the url matches any of the provider's code prefixes,
* `channel` is set to the url "channel" query parameter.
send_in_pings: send_in_pings:
- metrics - metrics
- baseline - baseline
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/6558 - https://github.com/mozilla-mobile/fenix/issues/6558
- https://github.com/mozilla-mobile/fenix/issues/28010
- https://bugzilla.mozilla.org/show_bug.cgi?id=1799049
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10112 - https://github.com/mozilla-mobile/fenix/pull/10112
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938 - https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938
- https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301 - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301
- https://github.com/mozilla-mobile/fenix/pull/28012#issuecomment-1330822281
data_sensitivity: data_sensitivity:
- interaction - interaction
notification_emails: notification_emails:
@ -6224,18 +6265,27 @@ browser.search:
type: labeled_counter type: labeled_counter
description: | description: |
Records clicks of adverts on SERP pages. Records clicks of adverts on SERP pages.
The key format is `<provider-name>`. The key format is
`<provider-name>.in-content.[sap|sap-follow-on|organic].[code|none](.[channel])?`,
where:
* `provider-name` is the name of the provider,
* `sap|sap-follow-on|organic` is the search access point,
* `code` is set when the url matches any of the provider's code prefixes,
* `channel` is set to the url "channel" query parameter.
send_in_pings: send_in_pings:
- metrics - metrics
- baseline - baseline
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/6558 - https://github.com/mozilla-mobile/fenix/issues/6558
- https://github.com/mozilla-mobile/fenix/issues/28010
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10112 - https://github.com/mozilla-mobile/fenix/pull/10112
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
- https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938 - https://github.com/mozilla-mobile/fenix/pull/20230#issuecomment-879244938
- https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301 - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301
- https://github.com/mozilla-mobile/fenix/pull/28012#issuecomment-1330822281
data_sensitivity: data_sensitivity:
- interaction - interaction
notification_emails: notification_emails:
@ -6795,6 +6845,93 @@ autoplay:
tags: tags:
- SitePermissions - SitePermissions
cookie_banners:
visited_setting:
type: event
description: A user visited the cookie banner handling screen
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1796146
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27561
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
setting_changed:
type: event
description: |
A user changed their setting.
extra_keys:
cookie_banner_setting:
description: |
The new setting for cookie banners: disabled,reject_all,
or reject_or_accept_all.
type: string
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1796146
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/27561
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
exception_added:
type: event
description: |
A user added a cookie banner handling exception through
the toggle in the protections panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
exception_removed:
type: event
description: |
A user removed a cookie banner handling
exception through the toggle in the protections panel.
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
visited_panel:
type: event
description: A user visited the cookie banner toolbar panel
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1797577
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: 118
metadata:
tags:
- Privacy&Security
site_permissions: site_permissions:
prompt_shown: prompt_shown:
type: event type: event

@ -18,6 +18,7 @@ object Constants {
} }
const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH" const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH"
const val POCKET_RECOMMENDED_STORIES_UTM_PARAM = "utm_source=pocket-newtab-android"
const val LONG_CLICK_DURATION: Long = 5000 const val LONG_CLICK_DURATION: Long = 5000
const val LISTS_MAXSWIPES: Int = 3 const val LISTS_MAXSWIPES: Int = 3
const val RETRY_COUNT = 3 const val RETRY_COUNT = 3

@ -0,0 +1,19 @@
/* 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.helpers
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.appContext
object Experimentation {
val experiments =
appContext.components.analytics.experiments
fun withHelper(block: GleanPlumbMessageHelper.() -> Unit) {
val helper = experiments.createMessageHelper()
block(helper)
}
}

@ -6,6 +6,7 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.content.Intent
import android.view.ViewConfiguration.getLongPressTimeout import android.view.ViewConfiguration.getLongPressTimeout
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
@ -161,6 +162,8 @@ class HomeActivityIntentTestRule internal constructor(
private val longTapUserPreference = getLongPressTimeout() private val longTapUserPreference = getLongPressTimeout()
private lateinit var intent: Intent
/** /**
* Update settings after the activity was created. * Update settings after the activity was created.
*/ */
@ -171,6 +174,19 @@ class HomeActivityIntentTestRule internal constructor(
} }
} }
override fun getActivityIntent(): Intent? {
return if (this::intent.isInitialized) {
this.intent
} else {
super.getActivityIntent()
}
}
fun withIntent(intent: Intent): HomeActivityIntentTestRule {
this.intent = intent
return this
}
override fun beforeActivityLaunched() { override fun beforeActivityLaunched() {
super.beforeActivityLaunched() super.beforeActivityLaunched()
setLongTapTimeout(3000) setLongTapTimeout(3000)

@ -6,6 +6,7 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.Manifest
import android.app.ActivityManager import android.app.ActivityManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
@ -20,8 +21,12 @@ import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.Settings import android.provider.Settings
import android.util.Log
import android.view.View import android.view.View
import androidx.annotation.RequiresApi
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
@ -38,10 +43,10 @@ import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import androidx.test.runner.permission.PermissionRequester
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
@ -52,12 +57,13 @@ import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.junit.Assert import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -65,6 +71,7 @@ import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
import org.mozilla.fenix.ui.robots.BrowserRobot import org.mozilla.fenix.ui.robots.BrowserRobot
import org.mozilla.fenix.utils.IntentUtils import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.gecko.util.ThreadUtils import org.mozilla.gecko.util.ThreadUtils
import java.io.File
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern import java.util.regex.Pattern
@ -147,24 +154,21 @@ object TestHelper {
} }
} }
// Remove test file from Google Photos (AOSP) on Firebase @RequiresApi(Build.VERSION_CODES.R)
fun deleteDownloadFromStorage() { fun deleteDownloadedFileOnStorage(fileName: String) {
val deleteButton = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/trash")) val storageManager: StorageManager? = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
deleteButton.waitForExists(waitingTime) val storageVolumes = storageManager!!.storageVolumes
deleteButton.click() val storageVolume: StorageVolume = storageVolumes[0]
val file = File(storageVolume.directory!!.path + "/Download/" + fileName)
// Sometimes there's a secondary confirmation
try { try {
val deleteConfirm = mDevice.findObject(UiSelector().text("Got it")) file.delete()
deleteConfirm.waitForExists(waitingTime) Log.d("TestLog", "File delete try 1")
deleteConfirm.click() assertFalse("The file was not deleted", file.exists())
} catch (e: UiObjectNotFoundException) { } catch (e: AssertionError) {
// Do nothing file.delete()
Log.d("TestLog", "File delete retried")
assertFalse("The file was not deleted", file.exists())
} }
val trashIt = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/move_to_trash"))
trashIt.waitForExists(waitingTime)
trashIt.click()
} }
fun setNetworkEnabled(enabled: Boolean) { fun setNetworkEnabled(enabled: Boolean) {
@ -388,20 +392,31 @@ object TestHelper {
/** /**
* Changes the default language of the entire device, not just the app. * Changes the default language of the entire device, not just the app.
* Runs on Debug variant as we don't want to adjust Release permission manifests
* Runs the test in its testBlock. * Runs the test in its testBlock.
* Cleans up and sets the default locale after it's are done. * Cleans up and sets the default locale after it's done.
*/ */
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) { fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
val defaultLocale = Locale.getDefault() if (Config.channel.isDebug) {
/* Sets permission to change device language */
PermissionRequester().apply {
addPermissions(
Manifest.permission.CHANGE_CONFIGURATION,
)
requestPermissions()
}
try { val defaultLocale = Locale.getDefault()
setSystemLocale(locale)
testBlock() try {
ThreadUtils.runOnUiThread { testRule.activity.recreate() } setSystemLocale(locale)
} catch (e: Exception) { testBlock()
e.printStackTrace() ThreadUtils.runOnUiThread { testRule.activity.recreate() }
} finally { } catch (e: Exception) {
setSystemLocale(defaultLocale) e.printStackTrace()
} finally {
setSystemLocale(defaultLocale)
}
} }
} }

@ -33,7 +33,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
* *
* Say no to main thread IO! 🙅 * Say no to main thread IO! 🙅
*/ */
private const val EXPECTED_SUPPRESSION_COUNT = 19 private const val EXPECTED_SUPPRESSION_COUNT = 18
/** /**
* The number of times we call the `runBlocking` coroutine method on the main thread during this * The number of times we call the `runBlocking` coroutine method on the main thread during this

@ -128,6 +128,28 @@ class ContextMenusTest {
} }
} }
@Test
fun verifyContextCopyLinkNotDisplayedAfterApplied() {
val pageLinks = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(pageLinks.url) {
longClickMatchingText("Link 3")
verifyLinkContextMenuItems(genericURL.url)
clickContextCopyLink()
verifySnackBarText("Link copied to clipboard")
}.openNavigationToolbar {
}.visitLinkFromClipboard {
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
}.openNewTab {
}
navigationToolbar {
verifyClipboardSuggestionsAreDisplayed(shouldBeDisplayed = false)
}
}
@Test @Test
fun verifyContextShareLink() { fun verifyContextShareLink() {
val pageLinks = val pageLinks =

@ -5,20 +5,14 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.permission.PermissionRequester
import androidx.test.uiautomator.UiDevice
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.rules.TestRule
import org.junit.rules.TestWatcher
import org.junit.runner.Description
import org.mozilla.fenix.customannotations.SmokeTest import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -33,8 +27,6 @@ import org.mozilla.fenix.ui.robots.notificationShade
* - Verifies managing downloads inside the Downloads listing. * - Verifies managing downloads inside the Downloads listing.
**/ **/
class DownloadTest { class DownloadTest {
private lateinit var mDevice: UiDevice
/* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */ /* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */
private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html" private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
private var downloadFile: String = "" private var downloadFile: String = ""
@ -42,25 +34,8 @@ class DownloadTest {
@get:Rule @get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides() val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
// Making sure to grant storage access for this test running on API 28
@get: Rule
var watcher: TestRule = object : TestWatcher() {
override fun starting(description: Description) {
if (description.methodName == "pauseResumeCancelDownloadTest") {
PermissionRequester().apply {
addPermissions(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE,
)
requestPermissions()
}
}
}
}
@Before @Before
fun setUp() { fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// clear all existing notifications // clear all existing notifications
notificationShade { notificationShade {
mDevice.openNotification() mDevice.openNotification()
@ -157,13 +132,13 @@ class DownloadTest {
/* Verifies downloads in the Downloads Menu: /* Verifies downloads in the Downloads Menu:
- downloads appear in the list - downloads appear in the list
- deleting a download from device storage, removes it from the Downloads Menu too - deleting a download from device storage, removes it from the Downloads Menu too
*/ */
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/27220")
@SmokeTest @SmokeTest
@Test @Test
fun manageDownloadsInDownloadsMenuTest() { fun manageDownloadsInDownloadsMenuTest() {
// a long filename to verify it's correctly displayed on the prompt and in the Downloads menu // a long filename to verify it's correctly displayed on the prompt and in the Downloads menu
downloadFile = "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg" downloadFile =
"tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg"
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) { }.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
@ -179,14 +154,34 @@ class DownloadTest {
waitForDownloadsListToExist() waitForDownloadsListToExist()
verifyDownloadedFileName(downloadFile) verifyDownloadedFileName(downloadFile)
verifyDownloadedFileIcon() verifyDownloadedFileIcon()
openDownloadedFile(downloadFile) deleteDownloadedFileOnStorage(downloadFile)
verifyPhotosAppOpens()
deleteDownloadFromStorage()
waitForDownloadsListToExist()
}.exitDownloadsManagerToBrowser { }.exitDownloadsManagerToBrowser {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openDownloadsManager { }.openDownloadsManager {
verifyEmptyDownloadsList() verifyEmptyDownloadsList()
} }
} }
@SmokeTest
@Test
fun openDownloadedFileTest() {
downloadFile = "web_icon.png"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
verifyDownloadNotificationPopup()
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyDownloadedFileName(downloadFile)
openDownloadedFile(downloadFile)
verifyPhotosAppOpens()
mDevice.pressBack()
}
}
} }

@ -4,6 +4,7 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
@ -11,9 +12,11 @@ import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
@ -34,11 +37,13 @@ class HomeScreenTest {
private lateinit var mDevice: UiDevice private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private lateinit var firstPocketStoryPublisher: String
@get:Rule @get:Rule(order = 0)
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides() val activityTestRule =
AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity }
@Rule @Rule(order = 1)
@JvmField @JvmField
val retryTestRule = RetryTestRule(3) val retryTestRule = RetryTestRule(3)
@ -62,21 +67,26 @@ class HomeScreenTest {
homeScreen { }.dismissOnboarding() homeScreen { }.dismissOnboarding()
homeScreen { homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark() verifyHomeWordmark()
verifyTabButton() verifyHomePrivateBrowsingButton()
verifyCollectionsHeader()
verifyHomeToolbar()
verifyHomeComponent()
// Verify Top Sites
verifyExistingTopSitesList()
verifyExistingTopSitesTabs("Wikipedia") verifyExistingTopSitesTabs("Wikipedia")
verifyExistingTopSitesTabs("Top Articles") verifyExistingTopSitesTabs("Top Articles")
verifyExistingTopSitesTabs("Google") verifyExistingTopSitesTabs("Google")
verifyCollectionsHeader()
verifyNoCollectionsText()
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7)
verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8)
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
verifyStoriesByTopicItems()
verifyPoweredByPocket(activityTestRule)
verifyCustomizeHomepageButton(true)
verifyNavigationToolbar()
verifyDefaultSearchEngine("Google")
verifyHomeMenuButton()
verifyTabButton()
verifyTabCounter("0")
} }
} }
@ -89,11 +99,11 @@ class HomeScreenTest {
verifyHomeScreen() verifyHomeScreen()
verifyNavigationToolbar() verifyNavigationToolbar()
verifyHomePrivateBrowsingButton() verifyHomePrivateBrowsingButton()
verifyHomeMenu() verifyHomeMenuButton()
verifyHomeWordmark() verifyHomeWordmark()
verifyTabButton() verifyTabButton()
verifyPrivateSessionMessage() verifyPrivateSessionMessage()
verifyHomeToolbar() verifyNavigationToolbar()
verifyHomeComponent() verifyHomeComponent()
} }
@ -104,15 +114,47 @@ class HomeScreenTest {
verifyHomeScreen() verifyHomeScreen()
verifyNavigationToolbar() verifyNavigationToolbar()
verifyHomePrivateBrowsingButton() verifyHomePrivateBrowsingButton()
verifyHomeMenu() verifyHomeMenuButton()
verifyHomeWordmark() verifyHomeWordmark()
verifyTabButton() verifyTabButton()
verifyPrivateSessionMessage() verifyPrivateSessionMessage()
verifyHomeToolbar() verifyNavigationToolbar()
verifyHomeComponent() verifyHomeComponent()
} }
} }
@Test
fun verifyJumpBackInSectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 4)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(firstWebPage.title)
verifyJumpBackInItemWithUrl(firstWebPage.url.toString())
verifyJumpBackInShowAllButton()
}.clickJumpBackInShowAllButton {
verifyExistingOpenTabs(firstWebPage.title)
}.closeTabDrawer() {
}
homeScreen {
}.clickJumpBackInItemWithTitle(firstWebPage.title) {
verifyUrl(firstWebPage.url.toString())
clickLinkMatchingText("Link 1")
}.goToHomescreen {
verifyJumpBackInSectionIsDisplayed()
verifyJumpBackInItemTitle(secondWebPage.title)
verifyJumpBackInItemWithUrl(secondWebPage.url.toString())
}.openTabDrawer {
closeTab()
}
homeScreen {
verifyJumpBackInSectionIsNotDisplayed()
}
}
@Test @Test
fun dismissOnboardingUsingSettingsTest() { fun dismissOnboardingUsingSettingsTest() {
homeScreen { homeScreen {
@ -141,7 +183,7 @@ class HomeScreenTest {
@Test @Test
fun dismissOnboardingUsingHelpTest() { fun dismissOnboardingUsingHelpTest() {
activityTestRule.applySettingsExceptions { activityTestRule.activityRule.applySettingsExceptions {
it.isJumpBackInCFREnabled = false it.isJumpBackInCFREnabled = false
it.isWallpaperOnboardingEnabled = false it.isWallpaperOnboardingEnabled = false
} }
@ -156,6 +198,25 @@ class HomeScreenTest {
} }
} }
@Test
fun dismissOnboardingWithPageLoadTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isJumpBackInCFREnabled = false
it.isWallpaperOnboardingEnabled = false
}
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
verifyWelcomeHeader()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.goToHomescreen {
verifyExistingTopSitesList()
}
}
@Test @Test
fun toolbarTapDoesntDismissOnboardingTest() { fun toolbarTapDoesntDismissOnboardingTest() {
homeScreen { homeScreen {
@ -171,7 +232,7 @@ class HomeScreenTest {
@Test @Test
fun verifyPocketHomepageStoriesTest() { fun verifyPocketHomepageStoriesTest() {
activityTestRule.applySettingsExceptions { activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false it.isRecentlyVisitedFeatureEnabled = false
} }
@ -181,6 +242,11 @@ class HomeScreenTest {
homeScreen { homeScreen {
verifyThoughtProvokingStories(true) verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7)
verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8)
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
verifyStoriesByTopic(true) verifyStoriesByTopic(true)
}.openThreeDotMenu { }.openThreeDotMenu {
}.openCustomizeHome { }.openCustomizeHome {
@ -191,6 +257,79 @@ class HomeScreenTest {
} }
} }
@Test
fun openPocketStoryItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
firstPocketStoryPublisher = getProvokingStoryPublisher(1)
}.clickPocketStoryItem(firstPocketStoryPublisher, 1) {
verifyUrl(POCKET_RECOMMENDED_STORIES_UTM_PARAM)
}
}
@Ignore("Failed, see: https://github.com/mozilla-mobile/fenix/issues/28098")
@Test
fun openPocketDiscoverMoreTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
}.clickPocketDiscoverMoreButton(activityTestRule, 9) {
verifyUrl("getpocket.com/explore")
}
}
@Test
fun selectStoriesByTopicItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyStoriesByTopicItemState(activityTestRule, false, 1)
clickStoriesByTopicItem(activityTestRule, 1)
verifyStoriesByTopicItemState(activityTestRule, true, 1)
}
}
@Test
fun verifyPocketLearnMoreLinkTest() {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
homeScreen {
}.dismissOnboarding()
homeScreen {
verifyPoweredByPocket(activityTestRule)
}.clickPocketLearnMoreLink(activityTestRule) {
verifyUrl("mozilla.org/en-US/firefox/pocket")
}
}
@Test @Test
fun verifyCustomizeHomepageTest() { fun verifyCustomizeHomepageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -12,11 +12,14 @@ import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.runWithSystemLocaleChanged
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import java.util.Locale
/** /**
* Tests for verifying basic functionality of browser navigation and page related interactions * Tests for verifying basic functionality of browser navigation and page related interactions
@ -95,6 +98,46 @@ class NavigationToolbarTest {
} }
} }
// Swipes the nav bar left/right to switch between tabs
@SmokeTest
@Test
fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString())
verifyUrl(firstWebPage.url.toString())
swipeNavBarLeft(firstWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
// Because it requires changing system prefs, this test will run only on Debug builds
@Test
fun swipeToSwitchTabInRTLTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val arabicLocale = Locale("ar", "AR")
runWithSystemLocaleChanged(arabicLocale, activityTestRule) {
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarLeft(secondWebPage.url.toString())
verifyUrl(firstWebPage.url.toString())
swipeNavBarRight(firstWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
}
// Test running on beta/release builds in CI: // Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds // caution when making changes to it, so they don't block the builds
@Test @Test

@ -0,0 +1,74 @@
/* 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 android.content.Intent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import io.mockk.mockk
import mozilla.components.concept.sync.AuthType
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.components.TelemetryAccountObserver
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Experimentation
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.ui.robots.homeScreen
class NimbusEventTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@get:Rule
val homeActivityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
.withIntent(
Intent().apply {
action = Intent.ACTION_VIEW
},
)
@Rule
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@Test
fun homeScreenNimbusEventsTest() {
homeScreen { }.dismissOnboarding()
Experimentation.withHelper {
assertTrue(evalJexl("'app_opened'|eventSum('Days', 28, 0) > 0"))
}
}
@Test
fun telemetryAccountObserverTest() {
val observer = TelemetryAccountObserver(appContext)
observer.onAuthenticated(mockk(), AuthType.Signin)
Experimentation.withHelper {
assertTrue(evalJexl("'sync_auth.sign_in'|eventSum('Days', 28, 0) > 0"))
}
}
}

@ -160,6 +160,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest @SmokeTest
@Test @Test
fun searchGroupShowsInRecentlyVisitedTest() { fun searchGroupShowsInRecentlyVisitedTest() {
@ -194,6 +195,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test @Test
fun verifySearchGroupHistoryWithNoDuplicatesTest() { fun verifySearchGroupHistoryWithNoDuplicatesTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url val firstPageUrl = getGenericAsset(searchMockServer, 1).url
@ -275,6 +277,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest @SmokeTest
@Test @Test
fun noSearchGroupFromPrivateBrowsingTest() { fun noSearchGroupFromPrivateBrowsingTest() {
@ -313,6 +316,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest @SmokeTest
@Test @Test
fun deleteItemsFromSearchGroupHistoryTest() { fun deleteItemsFromSearchGroupHistoryTest() {
@ -361,6 +365,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test @Test
fun deleteSearchGroupFromHistoryTest() { fun deleteSearchGroupFromHistoryTest() {
queryString = "test search" queryString = "test search"
@ -407,6 +412,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test @Test
fun reopenTabsFromSearchGroupTest() { fun reopenTabsFromSearchGroupTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url val firstPageUrl = getGenericAsset(searchMockServer, 1).url
@ -460,6 +466,7 @@ class SearchTest {
} }
} }
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test @Test
fun sharePageFromASearchGroupTest() { fun sharePageFromASearchGroupTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url val firstPageUrl = getGenericAsset(searchMockServer, 1).url

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import android.content.res.Configuration
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -71,17 +70,6 @@ class SettingsBasicsTest {
mockWebServer.shutdown() mockWebServer.shutdown()
} }
private fun getUiTheme(): Boolean {
val mode =
activityIntentTestRule.activity.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)
return when (mode) {
Configuration.UI_MODE_NIGHT_YES -> true // dark theme is set
Configuration.UI_MODE_NIGHT_NO -> false // dark theme is not set, using light theme
else -> false // default option is light theme
}
}
@Test @Test
fun settingsGeneralItemsTests() { fun settingsGeneralItemsTests() {
homeScreen { homeScreen {
@ -101,21 +89,6 @@ class SettingsBasicsTest {
} }
} }
@Test
fun changeThemeSetting() {
// Goes through the settings and changes the default search engine, then verifies it changes.
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
verifyThemes()
selectDarkMode()
verifyDarkThemeApplied(getUiTheme())
selectLightMode()
verifyLightThemeApplied(getUiTheme())
}
}
@Test @Test
fun changeAccessibiltySettings() { fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.
@ -302,6 +275,7 @@ class SettingsBasicsTest {
} }
} }
// Because it requires changing system prefs, this test will run only on Debug builds
@Ignore("Failing due to app translation bug, see: https://github.com/mozilla-mobile/fenix/issues/26729") @Ignore("Failing due to app translation bug, see: https://github.com/mozilla-mobile/fenix/issues/26729")
@Test @Test
fun frenchSystemLocaleTest() { fun frenchSystemLocaleTest() {

@ -0,0 +1,122 @@
package org.mozilla.fenix.ui
import android.content.res.Configuration
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
class SettingsCustomizeTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
private fun getUiTheme(): Boolean {
val mode =
activityIntentTestRule.activity.resources?.configuration?.uiMode?.and(Configuration.UI_MODE_NIGHT_MASK)
return when (mode) {
Configuration.UI_MODE_NIGHT_YES -> true // dark theme is set
Configuration.UI_MODE_NIGHT_NO -> false // dark theme is not set, using light theme
else -> false // default option is light theme
}
}
@Test
fun changeThemeSettingTest() {
// Goes through the settings and changes the default search engine, then verifies it changes.
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
verifyThemes()
selectDarkMode()
verifyDarkThemeApplied(getUiTheme())
selectLightMode()
verifyLightThemeApplied(getUiTheme())
}
}
@Test
fun setToolbarPositionTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
verifyToolbarPositionPreference("Bottom")
clickTopToolbarToggle()
verifyToolbarPositionPreference("Top")
}.goBack {
}.goBack {
verifyToolbarPosition(defaultPosition = false)
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickBottomToolbarToggle()
verifyToolbarPositionPreference("Bottom")
exitMenu()
}
homeScreen {
verifyToolbarPosition(defaultPosition = true)
}
}
@Test
fun swipeToolbarGesturePreferenceOffTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
verifySwipeToolbarGesturePrefState(true)
clickSwipeToolbarToSwitchTabToggle()
verifySwipeToolbarGesturePrefState(false)
exitMenu()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
swipeNavBarLeft(secondWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
@Test
fun pullToRefreshPreferenceTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
verifyPullToRefreshGesturePrefState(isEnabled = true)
clickPullToRefreshToggle()
verifyPullToRefreshGesturePrefState(isEnabled = false)
}
}
}

@ -15,6 +15,8 @@ import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RetryTestRule import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.openAppFromExternalLink
import org.mozilla.fenix.helpers.TestHelper.restartApp import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
@ -180,19 +182,6 @@ class SettingsHomepageTest {
fun startOnLastTabTest() { fun startOnLastTabTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1) val firstWebPage = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHomepageSubMenu {
clickStartOnHomepageButton()
}
restartApp(activityIntentTestRule)
homeScreen {
verifyHomeScreen()
}
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) { }.enterURLAndEnterToBrowser(firstWebPage.url) {
}.goToHomescreen { }.goToHomescreen {
@ -208,6 +197,31 @@ class SettingsHomepageTest {
} }
} }
@Test
fun ignoreStartOnHomeWhenLaunchedByExternalLinkTest() {
val genericPage = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHomepageSubMenu {
clickStartOnHomepageButton()
}.goBack {}
with(activityIntentTestRule) {
finishActivity()
mDevice.waitForIdle()
this.applySettingsExceptions {
it.isTCPCFREnabled = false
}
openAppFromExternalLink(genericPage.url.toString())
}
browserScreen {
verifyPageContent(genericPage.content)
}
}
@SmokeTest @SmokeTest
@Test @Test
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/26559") @Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/26559")

@ -420,12 +420,11 @@ class SettingsSearchTest {
"Bing", "Bing",
"Amazon.com", "Amazon.com",
"DuckDuckGo", "DuckDuckGo",
"eBay", "ويكيبيديا (ar)",
/* Disabled Arabic Wikipedia verification
until https://github.com/mozilla-mobile/fenix/issues/12236 gets fixed
"ويكيبيديا (ar)"
*/
) )
changeDefaultSearchEngine(activityTestRule, "ويكيبيديا (ar)")
}.submitQuery("firefox") {
verifyUrl("ar.m.wikipedia.org")
} }
} }

@ -37,7 +37,6 @@ import org.mozilla.fenix.helpers.TestHelper.assertNativeAppOpens
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.generateRandomString import org.mozilla.fenix.helpers.TestHelper.generateRandomString
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.customTabScreen import org.mozilla.fenix.ui.robots.customTabScreen
@ -46,7 +45,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade import org.mozilla.fenix.ui.robots.notificationShade
import org.mozilla.fenix.ui.robots.openEditURLView import org.mozilla.fenix.ui.robots.openEditURLView
import org.mozilla.fenix.ui.robots.searchScreen import org.mozilla.fenix.ui.robots.searchScreen
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/** /**
* Test Suite that contains a part of the Smoke and Sanity tests defined in TestRail: * Test Suite that contains a part of the Smoke and Sanity tests defined in TestRail:
@ -112,41 +110,19 @@ class SmokeTest {
@Test @Test
fun firstRunScreenTest() { fun firstRunScreenTest() {
homeScreen { homeScreen {
verifyHomeScreen() verifyHomeScreenAppBarItems()
verifyNavigationToolbar() verifyHomeScreenWelcomeItems()
verifyHomePrivateBrowsingButton() verifyChooseYourThemeCard(
verifyHomeMenu() isDarkThemeChecked = false,
verifyHomeWordmark() isLightThemeChecked = false,
isAutomaticThemeChecked = true,
verifyWelcomeHeader() )
// Sign in to Firefox verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
verifyStartSyncHeader() verifySignInToSyncCard()
verifyAccountsSignInButton() verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyNoticeCard()
// Always-on privacy verifyStartBrowsingSection()
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) verifyNavigationToolbarItems("0")
verifyAutomaticPrivacyHeader()
verifyAutomaticPrivacyText()
// Choose your theme
verifyChooseThemeHeader()
verifyChooseThemeText()
verifyDarkThemeDescription()
verifyDarkThemeToggle()
verifyLightThemeDescription()
verifyLightThemeToggle()
// Pick your toolbar placement
verifyTakePositionHeader()
verifyTakePositionElements()
// Your privacy
verifyYourPrivacyHeader()
verifyYourPrivacyText()
verifyPrivacyNoticeButton()
// Start Browsing
verifyStartBrowsingButton()
} }
} }
@ -421,24 +397,6 @@ class SmokeTest {
} }
} }
// Swipes the nav bar left/right to switch between tabs
@Test
fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString())
verifyUrl(firstWebPage.url.toString())
swipeNavBarLeft(firstWebPage.url.toString())
verifyUrl(secondWebPage.url.toString())
}
}
// Saves a login, then changes it and verifies the update // Saves a login, then changes it and verifies the update
@Test @Test
fun updateSavedLoginTest() { fun updateSavedLoginTest() {

@ -122,7 +122,7 @@ class TabbedBrowsingTest {
verifyShareTabButton() verifyShareTabButton()
verifySelectTabs() verifySelectTabs()
}.closeAllTabs { }.closeAllTabs {
verifyNoTabsOpened() verifyTabCounter("0")
} }
// Repeat for Private Tabs // Repeat for Private Tabs
@ -137,7 +137,7 @@ class TabbedBrowsingTest {
}.openTabsListThreeDotMenu { }.openTabsListThreeDotMenu {
verifyCloseAllTabsButton() verifyCloseAllTabsButton()
}.closeAllTabs { }.closeAllTabs {
verifyNoTabsOpened() verifyTabCounter("0")
} }
} }
@ -153,7 +153,7 @@ class TabbedBrowsingTest {
closeTab() closeTab()
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) { }.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer { }.openTabDrawer {
@ -161,7 +161,7 @@ class TabbedBrowsingTest {
swipeTabRight("Test_Page_1") swipeTabRight("Test_Page_1")
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) { }.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer { }.openTabDrawer {
@ -169,7 +169,7 @@ class TabbedBrowsingTest {
swipeTabLeft("Test_Page_1") swipeTabLeft("Test_Page_1")
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
} }
} }
@ -213,7 +213,7 @@ class TabbedBrowsingTest {
closeTab() closeTab()
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) { }.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer { }.openTabDrawer {
@ -221,7 +221,7 @@ class TabbedBrowsingTest {
swipeTabRight("Test_Page_1") swipeTabRight("Test_Page_1")
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) { }.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer { }.openTabDrawer {
@ -229,7 +229,7 @@ class TabbedBrowsingTest {
swipeTabLeft("Test_Page_1") swipeTabLeft("Test_Page_1")
} }
homeScreen { homeScreen {
verifyNoTabsOpened() verifyTabCounter("0")
} }
} }
@ -354,7 +354,7 @@ class TabbedBrowsingTest {
// dismiss search dialog // dismiss search dialog
homeScreen { }.pressBack() homeScreen { }.pressBack()
verifyPrivateSessionMessage() verifyPrivateSessionMessage()
verifyHomeToolbar() verifyNavigationToolbar()
} }
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
@ -365,7 +365,7 @@ class TabbedBrowsingTest {
// dismiss search dialog // dismiss search dialog
homeScreen { }.pressBack() homeScreen { }.pressBack()
verifyHomeWordmark() verifyHomeWordmark()
verifyHomeToolbar() verifyNavigationToolbar()
} }
} }
} }

@ -9,6 +9,7 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -19,6 +20,7 @@ import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.generateRandomString import org.mozilla.fenix.helpers.TestHelper.generateRandomString
import org.mozilla.fenix.helpers.TestHelper.getStringResource import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.waitUntilSnackbarGone
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -227,6 +229,8 @@ class TopSitesTest {
verifyExistingTopSitesTabs(defaultWebPage.title) verifyExistingTopSitesTabs(defaultWebPage.title)
}.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) { }.openContextMenuOnTopSitesWithTitle(defaultWebPage.title) {
}.deleteTopSiteFromHistory { }.deleteTopSiteFromHistory {
verifySnackBarText(getStringResource(R.string.snackbar_top_site_removed))
waitUntilSnackbarGone()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyEmptyHistoryView() verifyEmptyHistoryView()
@ -275,6 +279,7 @@ class TopSitesTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/25926")
@Test @Test
fun verifySponsoredShortcutsSponsorsAndPrivacyOptionTest() { fun verifySponsoredShortcutsSponsorsAndPrivacyOptionTest() {
var sponsoredShortcutTitle = "" var sponsoredShortcutTitle = ""

@ -8,15 +8,23 @@ package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap import android.graphics.Bitmap
import android.widget.EditText import android.widget.EditText
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onChildAt
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.RootMatchers
@ -57,9 +65,6 @@ import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.withBitmapDrawable import org.mozilla.fenix.helpers.withBitmapDrawable
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/** /**
* Implementation of Robot Pattern for the home screen menu. * Implementation of Robot Pattern for the home screen menu.
@ -71,19 +76,93 @@ class HomeScreenRobot {
" service provider, it makes it easier to keep what you do online private from anyone" + " service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device." " else who uses this device."
fun verifyNavigationToolbar() = assertNavigationToolbar() fun verifyNavigationToolbar() = assertAppItemsWithResourceId(navigationToolbar)
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar() fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen() fun verifyHomeScreen() = assertAppItemsWithResourceId(homeScreen)
fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton()
fun verifyHomeMenu() = assertHomeMenu() fun verifyHomeScreenAppBarItems() =
assertAppItemsWithResourceId(homeScreen, privateBrowsingButton, homepageWordmark)
fun verifyHomeScreenWelcomeItems() =
assertAppItemsContainingText(welcomeHeader, welcomeSubHeader)
fun verifyChooseYourThemeCard(
isDarkThemeChecked: Boolean,
isLightThemeChecked: Boolean,
isAutomaticThemeChecked: Boolean,
) {
scrollToElementByText(getStringResource(R.string.onboarding_theme_picker_header))
assertAppItemsContainingText(
chooseThemeHeader,
chooseThemeText,
darkThemeDescription,
lightThemeDescription,
)
assertAppItemsStateWithResourceId(
darkThemeToggle(isDarkThemeChecked),
lightThemeToggle(isLightThemeChecked),
automaticThemeToggle(isAutomaticThemeChecked),
)
assertAppItemsWithResourceIdAndDescription(automaticThemeDescription)
}
fun verifyToolbarPlacementCard(isBottomChecked: Boolean, isTopChecked: Boolean) {
scrollToElementByText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
assertAppItemsContainingText(toolbarPlacementHeader, toolbarPlacementDescription)
assertAppItemsStateWithResourceId(
toolbarPlacementBottomRadioButton(isBottomChecked),
toolbarPlacementTopRadioButton(isTopChecked),
)
assertAppItemsWithResourceId(
toolbarPlacementBottomImage,
toolbarPlacementBottomTitle,
toolbarPlacementTopImage,
toolbarPlacementTopTitle,
)
}
fun verifySignInToSyncCard() {
scrollToElementByText(getStringResource(R.string.onboarding_account_sign_in_header))
assertAppItemsContainingText(startSyncHeader, startSyncDescription)
assertAppItemsWithResourceId(signInButton)
}
fun verifyPrivacyProtectionCard(isStandardChecked: Boolean, isStrictChecked: Boolean) {
scrollToElementByText(getStringResource(R.string.onboarding_tracking_protection_header))
assertAppItemsContainingText(privacyProtectionHeader, privacyProtectionDescription)
assertAppItemsStateWithResourceId(
standardTrackingProtectionToggle(isStandardChecked),
strictTrackingProtectionToggle(isStrictChecked),
)
}
fun verifyPrivacyNoticeCard() {
scrollToElementByText(getStringResource(R.string.onboarding_privacy_notice_header_1))
assertAppItemsContainingText(privacyNoticeHeader, privacyNoticeDescription)
assertAppItemsWithResourceId(privacyNoticeButton)
}
fun verifyStartBrowsingSection() {
scrollToElementByText(getStringResource(R.string.onboarding_finish))
assertAppItemsWithResourceId(startBrowsingButton)
assertAppItemsContainingText(conclusionHeader)
}
fun verifyNavigationToolbarItems(numberOfOpenTabs: String) {
assertAppItemsWithResourceId(navigationToolbar, menuButton)
assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs))
}
fun verifyHomePrivateBrowsingButton() = assertAppItemsWithResourceId(privateBrowsingButton)
fun verifyHomeMenuButton() = assertAppItemsWithResourceId(menuButton)
fun verifyTabButton() = assertTabButton() fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText() fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertHomeWordmark() fun verifyHomeWordmark() = assertAppItemsWithResourceId(homepageWordmark)
fun verifyHomeToolbar() = assertHomeToolbar()
fun verifyHomeComponent() = assertHomeComponent() fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine) fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyNoTabsOpened() = assertNoTabsOpened() fun verifyTabCounter(numberOfOpenTabs: String) =
assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs))
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true) fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifyWallpaperImageApplied(isEnabled: Boolean) { fun verifyWallpaperImageApplied(isEnabled: Boolean) {
@ -105,33 +184,12 @@ class HomeScreenRobot {
} }
// First Run elements // First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader() fun verifyWelcomeHeader() = assertAppItemsContainingText(welcomeHeader)
fun verifyAccountsSignInButton() = assertAppItemsWithResourceId(signInButton)
fun verifyStartSyncHeader() = assertStartSyncHeader() fun verifyStartBrowsingButton() {
fun verifyAccountsSignInButton() = assertAccountsSignInButton() scrollToElementByText(getStringResource(R.string.onboarding_finish))
fun verifyChooseThemeHeader() = assertChooseThemeHeader() assertAppItemsWithResourceId(startBrowsingButton)
fun verifyChooseThemeText() = assertChooseThemeText() }
fun verifyLightThemeToggle() = assertLightThemeToggle()
fun verifyLightThemeDescription() = assertLightThemeDescription()
fun verifyDarkThemeToggle() = assertDarkThemeToggle()
fun verifyDarkThemeDescription() = assertDarkThemeDescription()
fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle()
fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription()
fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader()
fun verifyAutomaticPrivacyText() = assertAlwaysPrivacyText()
// Pick your toolbar placement
fun verifyTakePositionHeader() = assertTakePlacementHeader()
fun verifyTakePositionElements() {
assertTakePlacementBottomRadioButton()
assertTakePacementTopRadioButton()
}
// Your privacy
fun verifyYourPrivacyHeader() = assertYourPrivacyHeader()
fun verifyYourPrivacyText() = assertYourPrivacyText()
fun verifyPrivacyNoticeButton() = assertPrivacyNoticeButton()
fun verifyStartBrowsingButton() = assertStartBrowsingButton()
// Upgrading users onboarding dialog // Upgrading users onboarding dialog
fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) { fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) {
@ -182,6 +240,9 @@ class HomeScreenRobot {
fun verifyJumpBackInSectionIsDisplayed() = assertJumpBackInSectionIsDisplayed() fun verifyJumpBackInSectionIsDisplayed() = assertJumpBackInSectionIsDisplayed()
fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed() fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed()
fun verifyJumpBackInItemTitle(itemTitle: String) = assertJumpBackInItemTitle(itemTitle)
fun verifyJumpBackInItemWithUrl(itemUrl: String) = assertJumpBackInItemWithUrl(itemUrl)
fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton()
fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed() fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed()
fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed() fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed()
fun verifyRecentBookmarksSectionIsDisplayed() = assertRecentBookmarksSectionIsDisplayed() fun verifyRecentBookmarksSectionIsDisplayed() = assertRecentBookmarksSectionIsDisplayed()
@ -267,6 +328,39 @@ class HomeScreenRobot {
} }
} }
fun scrollToPocketProvokingStories() =
scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))
fun swipePocketProvokingStories() {
UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList()
.swipeLeft(3)
}
fun verifyPocketRecommendedStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
positions.forEach {
composeTestRule.onNodeWithTag("pocket.stories")
.onChildAt(it - 1)
.assert(hasTestTag("pocket.recommended.story"))
}
}
fun verifyPocketSponsoredStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
positions.forEach {
composeTestRule.onNodeWithTag("pocket.stories")
.onChildAt(it - 1)
.assert(hasTestTag("pocket.sponsored.story"))
}
}
fun verifyDiscoverMoreStoriesButton(composeTestRule: ComposeTestRule, position: Int) {
composeTestRule.onNodeWithTag("pocket.stories")
.assertIsDisplayed()
.onChildAt(position - 1)
.assert(hasTestTag("pocket.discover.more.story"))
}
fun verifyStoriesByTopic(enabled: Boolean) { fun verifyStoriesByTopic(enabled: Boolean) {
if (enabled) { if (enabled) {
scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header)) scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))
@ -291,6 +385,30 @@ class HomeScreenRobot {
} }
} }
fun verifyStoriesByTopicItems() =
assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.categories")).childCount > 1)
fun verifyStoriesByTopicItemState(composeTestRule: ComposeTestRule, isSelected: Boolean, position: Int) {
homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
if (isSelected) {
composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
storyByTopicItem(composeTestRule, position).assertIsSelected()
} else {
composeTestRule.onNodeWithTag("pocket.categories").assertIsDisplayed()
storyByTopicItem(composeTestRule, position).assertIsNotSelected()
}
}
fun clickStoriesByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
storyByTopicItem(composeTestRule, position).performClick()
fun verifyPoweredByPocket(rule: ComposeTestRule) {
homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
rule.onNodeWithTag("pocket.header.title", true).assertIsDisplayed()
rule.onNodeWithTag("pocket.header.subtitle", true).assertIsDisplayed()
}
fun verifyCustomizeHomepageButton(enabled: Boolean) { fun verifyCustomizeHomepageButton(enabled: Boolean) {
if (enabled) { if (enabled) {
scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1)) scrollToElementByText(getStringResource(R.string.browser_menu_customize_home_1))
@ -334,6 +452,31 @@ class HomeScreenRobot {
) )
} }
fun getProvokingStoryPublisher(position: Int): String {
val publisher = mDevice.findObject(
UiSelector()
.className("android.view.View")
.index(position - 1),
).getChild(
UiSelector()
.className("android.widget.TextView")
.index(1),
).text
return publisher
}
fun verifyToolbarPosition(defaultPosition: Boolean) {
onView(withId(R.id.toolbarLayout))
.check(
if (defaultPosition) {
isPartiallyBelow(withId(R.id.sessionControlRecyclerView))
} else {
isCompletelyAbove(withId(R.id.homeAppBar))
},
)
}
class Transition { class Transition {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
@ -366,8 +509,8 @@ class HomeScreenRobot {
} }
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar().waitForExists(waitingTime) navigationToolbar.waitForExists(waitingTime)
navigationToolbar().click() navigationToolbar.click()
SearchRobot().interact() SearchRobot().interact()
return SearchRobot.Transition() return SearchRobot.Transition()
@ -378,7 +521,7 @@ class HomeScreenRobot {
} }
fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition { fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
startBrowsingButton().click() startBrowsingButton.click()
SearchRobot().interact() SearchRobot().interact()
return SearchRobot.Transition() return SearchRobot.Transition()
@ -399,8 +542,7 @@ class HomeScreenRobot {
.waitForExists( .waitForExists(
waitingTime, waitingTime,
) )
privateBrowsingButton() privateBrowsingButton.click()
.perform(click())
} }
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
@ -411,8 +553,7 @@ class HomeScreenRobot {
waitingTime, waitingTime,
) )
privateBrowsingButton() privateBrowsingButton.click()
.perform(click())
} }
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
@ -426,7 +567,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar")) mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime) .waitForExists(waitingTime)
navigationToolbar().click() navigationToolbar.click()
NavigationToolbarRobot().interact() NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition() return NavigationToolbarRobot.Transition()
@ -582,6 +723,63 @@ class HomeScreenRobot {
SettingsSubMenuHomepageRobot().interact() SettingsSubMenuHomepageRobot().interact()
return SettingsSubMenuHomepageRobot.Transition() return SettingsSubMenuHomepageRobot.Transition()
} }
fun clickJumpBackInShowAllButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice
.findObject(
UiSelector()
.textContains(getStringResource(R.string.recent_tabs_show_all)),
).clickAndWaitForNewWindow(waitingTime)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
}
fun clickJumpBackInItemWithTitle(itemTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice
.findObject(
UiSelector()
.resourceId("recent.tab.title")
.textContains(itemTitle),
).clickAndWaitForNewWindow(waitingTime)
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickPocketStoryItem(publisher: String, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.findObject(
UiSelector()
.className("android.view.View")
.index(position - 1),
).getChild(
UiSelector()
.className("android.widget.TextView")
.index(1)
.textContains(publisher),
).clickAndWaitForNewWindow(waitingTime)
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickPocketDiscoverMoreButton(composeTestRule: ComposeTestRule, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.onNodeWithTag("pocket.stories")
.assertIsDisplayed()
.onChildAt(position - 1)
.assert(hasTestTag("pocket.discover.more.story"))
.performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickPocketLearnMoreLink(composeTestRule: ComposeTestRule, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.onNodeWithTag("pocket.header.subtitle", true).performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
} }
} }
@ -605,34 +803,10 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
.contains("mInputShown=true"), .contains("mInputShown=true"),
) )
private fun navigationToolbar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
private fun assertNavigationToolbar() = assertTrue(navigationToolbar().waitForExists(waitingTime))
private fun assertFocusedNavigationToolbar() = private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address"))) onView(allOf(withHint("Search or enter address")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomeScreen() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime)
onView(ViewMatchers.withResourceName("homeLayout"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomePrivateBrowsingButton() =
privateBrowsingButton()
.check(matches(isDisplayed()))
private val homepageWordmark = onView(ViewMatchers.withResourceName("wordmark"))
private fun assertHomeWordmark() =
homepageWordmark.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomeToolbar() = onView(ViewMatchers.withResourceName("toolbar"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertTabButton() = private fun assertTabButton() =
onView(allOf(withId(R.id.tab_button), isDisplayed())) onView(allOf(withId(R.id.tab_button), isDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -655,8 +829,6 @@ private fun assertHomeComponent() =
onView(ViewMatchers.withResourceName("sessionControlRecyclerView")) onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNoTabsOpened() = onView(withId(R.id.counter_text)).check(matches(withText("0")))
private fun threeDotButton() = onView(allOf(withId(R.id.menuButton))) private fun threeDotButton() = onView(allOf(withId(R.id.menuButton)))
private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: String) { private fun verifySearchEngineIcon(searchEngineIcon: Bitmap, searchEngineName: String) {
@ -668,141 +840,9 @@ private fun getSearchEngine(searchEngineName: String) =
appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName } appContext.components.core.store.state.search.searchEngines.find { it.name == searchEngineName }
private fun verifySearchEngineIcon(searchEngineName: String) { private fun verifySearchEngineIcon(searchEngineName: String) {
val ddgSearchEngine = getSearchEngine(searchEngineName) val defaultSearchEngine = getSearchEngine(searchEngineName)
?: throw AssertionError("No search engine with name $searchEngineName") ?: throw AssertionError("No search engine with name $searchEngineName")
verifySearchEngineIcon(ddgSearchEngine.icon, ddgSearchEngine.name) verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name)
}
// First Run elements
private fun assertWelcomeHeader() =
assertTrue(
mDevice.findObject(
UiSelector().textContains(
getStringResource(R.string.onboarding_header_2),
),
).waitForExists(waitingTime),
)
private fun assertStartSyncHeader() {
scrollToElementByText(STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER)
onView(allOf(withText(R.string.onboarding_account_sign_in_header)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAccountsSignInButton() {
scrollToElementByText(getStringResource(R.string.onboarding_firefox_account_sign_in))
onView(ViewMatchers.withResourceName("fxa_sign_in_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertChooseThemeHeader() {
scrollToElementByText("Choose your theme")
onView(withText("Choose your theme"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertChooseThemeText() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Save some battery and your eyesight with dark mode.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_light_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Light theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_dark_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Dark theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticThemeToggle() {
scrollToElementByText("Choose your theme")
onView(withId(R.id.theme_automatic_radio_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Automatic")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyHeader() {
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(allOf(withText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAlwaysPrivacyText() {
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(
allOf(
withText(
"Featuring Total Cookie Protection to stop trackers from using cookies to stalk you across sites.",
),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyHeader() {
scrollToElementByText("You control your data")
onView(allOf(withText("You control your data")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyText() {
scrollToElementByText("You control your data")
onView(
allOf(
withText(
"Firefox gives you control over what you share online and what you share with us.",
),
),
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertPrivacyNoticeButton() {
scrollToElementByText("You control your data")
onView(allOf(withText("Read our privacy notice")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertStartBrowsingButton() {
assertTrue(startBrowsingButton().waitForExists(waitingTime))
}
// Pick your toolbar placement
private fun assertTakePlacementHeader() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(allOf(withText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePacementTopRadioButton() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_top_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePlacementBottomRadioButton() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertPrivateSessionMessage() = private fun assertPrivateSessionMessage() =
@ -901,6 +941,35 @@ private fun assertJumpBackInSectionIsDisplayed() = assertTrue(jumpBackInSection(
private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort)) private fun assertJumpBackInSectionIsNotDisplayed() = assertFalse(jumpBackInSection().waitForExists(waitingTimeShort))
private fun assertJumpBackInItemTitle(itemTitle: String) =
assertTrue(
mDevice
.findObject(
UiSelector()
.resourceId("recent.tab.title")
.textContains(itemTitle),
).waitForExists(waitingTime),
)
private fun assertJumpBackInItemWithUrl(itemUrl: String) =
assertTrue(
mDevice
.findObject(
UiSelector()
.resourceId("recent.tab.url")
.textContains(itemUrl),
).waitForExists(waitingTime),
)
private fun assertJumpBackInShowAllButton() =
assertTrue(
mDevice
.findObject(
UiSelector()
.textContains(getStringResource(R.string.recent_tabs_show_all)),
).waitForExists(waitingTime),
)
private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime)) private fun assertRecentlyVisitedSectionIsDisplayed() = assertTrue(recentlyVisitedSection().waitForExists(waitingTime))
private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTime)) private fun assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTime))
@ -915,8 +984,6 @@ private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitFo
private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTime)) private fun assertPocketSectionIsNotDisplayed() = assertFalse(pocketSection().waitForExists(waitingTime))
private fun privateBrowsingButton() = onView(withId(R.id.privateBrowsingButton))
private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button)) private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))
private fun tabsCounter() = onView(withId(R.id.tab_button)) private fun tabsCounter() = onView(withId(R.id.tab_button))
@ -933,15 +1000,6 @@ private fun recentBookmarksSection() =
private fun pocketSection() = private fun pocketSection() =
mDevice.findObject(UiSelector().textContains(getStringResource(R.string.pocket_stories_header_1))) mDevice.findObject(UiSelector().textContains(getStringResource(R.string.pocket_stories_header_1)))
private fun startBrowsingButton(): UiObject {
val startBrowsingButton = mDevice.findObject(UiSelector().resourceId("$packageName:id/finish_button"))
homeScreenList()
.scrollIntoView(startBrowsingButton)
homeScreenList()
.ensureFullyVisible(startBrowsingButton)
return startBrowsingButton
}
private fun sponsoredShortcut(sponsoredShortcutTitle: String) = private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
mDevice.findObject( mDevice.findObject(
By By
@ -949,6 +1007,129 @@ private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
.textContains(sponsoredShortcutTitle), .textContains(sponsoredShortcutTitle),
) )
private fun storyByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
composeTestRule.onNodeWithTag("pocket.categories").onChildAt(position - 1)
private fun appItemWithResourceId(resourceId: String) =
mDevice.findObject(UiSelector().resourceId(resourceId))
private fun appItemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
private fun appItemStateWithResourceId(resourceId: String, state: Boolean) =
mDevice.findObject(UiSelector().resourceId(resourceId).checked(state))
private fun appItemWithResourceIdAndDescription(resourceId: String, description: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
private fun appItemWithResourceIdAndText(resourceId: String, text: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
private fun assertAppItemsWithResourceId(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsContainingText(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsStateWithResourceId(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsWithResourceIdAndDescription(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private fun assertAppItemsWithResourceIdAndText(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
}
private val homeScreen =
appItemWithResourceId("$packageName:id/homeLayout")
private val privateBrowsingButton =
appItemWithResourceId("$packageName:id/privateBrowsingButton")
private val homepageWordmark =
appItemWithResourceId("$packageName:id/wordmark")
private val welcomeHeader = appItemContainingText(getStringResource(R.string.onboarding_header_2))
private val welcomeSubHeader =
appItemContainingText(getStringResource(R.string.onboarding_message))
private val chooseThemeHeader =
appItemContainingText(getStringResource(R.string.onboarding_theme_picker_header))
private val chooseThemeText =
appItemContainingText(getStringResource(R.string.onboarding_theme_picker_description_2))
private val darkThemeDescription =
appItemContainingText(getStringResource(R.string.onboarding_theme_dark_title))
private val lightThemeDescription =
appItemContainingText(getStringResource(R.string.onboarding_theme_light_title))
private val automaticThemeDescription =
appItemWithResourceIdAndDescription(
"$packageName:id/clickable_region_automatic",
"${getStringResource(R.string.onboarding_theme_automatic_title)} ${getStringResource(R.string.onboarding_theme_automatic_summary)}",
)
private fun darkThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_dark_radio_button", isChecked)
private fun lightThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_light_radio_button", isChecked)
private fun automaticThemeToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/theme_automatic_radio_button", isChecked)
private val toolbarPlacementHeader =
appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_header_1))
private val toolbarPlacementDescription =
appItemContainingText(getStringResource(R.string.onboarding_toolbar_placement_description))
private fun toolbarPlacementBottomRadioButton(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/toolbar_bottom_radio_button", isChecked)
private fun toolbarPlacementTopRadioButton(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/toolbar_top_radio_button", isChecked)
private val toolbarPlacementBottomImage =
appItemWithResourceId("$packageName:id/toolbar_bottom_image")
private val toolbarPlacementBottomTitle =
appItemWithResourceId("$packageName:id/toolbar_bottom_title")
private val toolbarPlacementTopTitle =
appItemWithResourceId("$packageName:id/toolbar_top_title")
private val toolbarPlacementTopImage =
appItemWithResourceId("$packageName:id/toolbar_top_image")
private val startSyncHeader =
appItemContainingText(getStringResource(R.string.onboarding_account_sign_in_header))
private val startSyncDescription =
appItemContainingText(getStringResource(R.string.onboarding_manual_sign_in_description))
private val signInButton =
appItemWithResourceId("$packageName:id/fxa_sign_in_button")
private val privacyProtectionHeader =
appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_header))
private val privacyProtectionDescription =
appItemContainingText(getStringResource(R.string.onboarding_tracking_protection_description))
private fun standardTrackingProtectionToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/tracking_protection_standard_option", isChecked)
private fun strictTrackingProtectionToggle(isChecked: Boolean) =
appItemStateWithResourceId("$packageName:id/tracking_protection_strict_default", isChecked)
private val privacyNoticeHeader =
appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_header_1))
private val privacyNoticeDescription =
appItemContainingText(getStringResource(R.string.onboarding_privacy_notice_description))
private val privacyNoticeButton =
appItemWithResourceId("$packageName:id/read_button")
private val startBrowsingButton =
appItemWithResourceId("$packageName:id/finish_button")
private val conclusionHeader =
appItemContainingText(getStringResource(R.string.onboarding_conclusion_header))
private val navigationToolbar =
appItemWithResourceId("$packageName:id/toolbar")
private val menuButton =
appItemWithResourceId("$packageName:id/menuButton")
private fun tabCounter(numberOfOpenTabs: String) =
appItemWithResourceIdAndText("$packageName:id/counter_text", numberOfOpenTabs)
val deleteFromHistory = val deleteFromHistory =
onView( onView(
allOf( allOf(

@ -65,7 +65,7 @@ class NavigationToolbarRobot {
readerViewToggle().click() readerViewToggle().click()
} }
fun verifyClipboardSuggestionsAreDisplayed(link: String, shouldBeDisplayed: Boolean) { fun verifyClipboardSuggestionsAreDisplayed(link: String = "", shouldBeDisplayed: Boolean) {
when (shouldBeDisplayed) { when (shouldBeDisplayed) {
true -> { true -> {
assertTrue( assertTrue(

@ -537,3 +537,5 @@ private val awesomeBar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")) mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private val voiceSearchButton = mDevice.findObject(UiSelector().description("Voice search")) private val voiceSearchButton = mDevice.findObject(UiSelector().description("Voice search"))
private fun goBackButton() = onView(allOf(withContentDescription("Navigate up")))

@ -147,12 +147,12 @@ class SettingsRobot {
return SettingsSubMenuSearchRobot.Transition() return SettingsSubMenuSearchRobot.Transition()
} }
fun openCustomizeSubMenu(interact: SettingsSubMenuThemeRobot.() -> Unit): SettingsSubMenuThemeRobot.Transition { fun openCustomizeSubMenu(interact: SettingsSubMenuCustomizeRobot.() -> Unit): SettingsSubMenuCustomizeRobot.Transition {
fun customizeButton() = onView(withText("Customize")) fun customizeButton() = onView(withText("Customize"))
customizeButton().click() customizeButton().click()
SettingsSubMenuThemeRobot().interact() SettingsSubMenuCustomizeRobot().interact()
return SettingsSubMenuThemeRobot.Transition() return SettingsSubMenuCustomizeRobot.Transition()
} }
fun openTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition { fun openTabsSubMenu(interact: SettingsSubMenuTabsRobot.() -> Unit): SettingsSubMenuTabsRobot.Transition {

@ -23,6 +23,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -97,7 +98,7 @@ private fun assertAboutToolbar() =
private fun assertVersionNumber() { private fun assertVersionNumber() {
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val packageInfo = context.packageManager.getPackageInfoCompat(context.packageName, 0)
val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString() val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo).toString()
val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n" val buildNVersion = "${packageInfo.versionName} (Build #$versionCode)\n"

@ -0,0 +1,137 @@
/* 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/. */
@file:Suppress("TooManyFunctions")
package org.mozilla.fenix.ui.robots
import android.os.Build
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isNotChecked
import androidx.test.espresso.matcher.ViewMatchers.withClassName
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matchers.endsWith
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.hasCousin
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Theme sub menu.
*/
class SettingsSubMenuCustomizeRobot {
fun verifyThemes() = assertThemes()
fun verifyLightThemeApplied(expected: Boolean) =
assertFalse("Light theme not selected", expected)
fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected)
fun selectDarkMode() = darkModeToggle().click()
fun selectLightMode() = lightModeToggle().click()
fun clickTopToolbarToggle() = topToolbarToggle().click()
fun clickBottomToolbarToggle() = bottomToolbarToggle().click()
fun verifyToolbarPositionPreference(selectedPosition: String) {
onView(withText(selectedPosition))
.check(matches(hasSibling(allOf(withId(R.id.radio_button), isChecked()))))
}
fun clickSwipeToolbarToSwitchTabToggle() = swipeToolbarToggle.click()
fun clickPullToRefreshToggle() = pullToRefreshToggle.click()
fun verifySwipeToolbarGesturePrefState(isEnabled: Boolean) {
swipeToolbarToggle
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (isEnabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
}
fun verifyPullToRefreshGesturePrefState(isEnabled: Boolean) {
pullToRefreshToggle
.check(
matches(
hasCousin(
allOf(
withClassName(endsWith("Switch")),
if (isEnabled) {
isChecked()
} else {
isNotChecked()
},
),
),
),
)
}
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
goBackButton().perform(ViewActions.click())
SettingsRobot().interact()
return SettingsRobot.Transition()
}
}
}
private fun assertThemes() {
lightModeToggle()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
darkModeToggle()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
deviceModeToggle()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun darkModeToggle() = onView(withText("Dark"))
private fun lightModeToggle() = onView(withText("Light"))
private fun topToolbarToggle() = onView(withText("Top"))
private fun bottomToolbarToggle() = onView(withText("Bottom"))
private fun deviceModeToggle(): ViewInteraction {
val followDeviceThemeText =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver"
return onView(withText(followDeviceThemeText))
}
private val swipeToolbarToggle =
onView(withText(getStringResource(R.string.preference_gestures_swipe_toolbar_switch_tabs)))
private val pullToRefreshToggle =
onView(withText(getStringResource(R.string.preference_gestures_website_pull_to_refresh)))
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -1,77 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("TooManyFunctions")
package org.mozilla.fenix.ui.robots
import android.os.Build
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Theme sub menu.
*/
class SettingsSubMenuThemeRobot {
fun verifyThemes() = assertThemes()
fun verifyLightThemeApplied(expected: Boolean) =
assertFalse("Light theme not selected", expected)
fun verifyDarkThemeApplied(expected: Boolean) = assertTrue("Dark theme not selected", expected)
fun selectDarkMode() = darkModeToggle().click()
fun selectLightMode() = lightModeToggle().click()
fun clickTopToolbarToggle() = topToolbarToggle().click()
fun clickBottomToolbarToggle() = bottomToolbarToggle().click()
class Transition {
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
mDevice.waitForIdle()
goBackButton().perform(ViewActions.click())
SettingsRobot().interact()
return SettingsRobot.Transition()
}
}
}
private fun assertThemes() {
lightModeToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
darkModeToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
deviceModeToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun darkModeToggle() = onView(withText("Dark"))
private fun lightModeToggle() = onView(withText("Light"))
private fun topToolbarToggle() = onView(withText("Top"))
private fun bottomToolbarToggle() = onView(withText("Bottom"))
private fun deviceModeToggle(): ViewInteraction {
val followDeviceThemeText =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) "Follow device theme" else "Set by Battery Saver"
return onView(withText(followDeviceThemeText))
}
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -4,9 +4,6 @@
package org.mozilla.fenix.ui.util package org.mozilla.fenix.ui.util
const val STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER = "Pick up where you left off"
const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Privacy protection by default"
const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement"
const val FRENCH_LANGUAGE_HEADER = "Langues" const val FRENCH_LANGUAGE_HEADER = "Langues"
const val ROMANIAN_LANGUAGE_HEADER = "Limbă" const val ROMANIAN_LANGUAGE_HEADER = "Limbă"
const val ARABIC_LANGUAGE_HEADER = "اللغة" const val ARABIC_LANGUAGE_HEADER = "اللغة"

@ -29,6 +29,9 @@
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" /> tools:ignore="QueryAllPackagesPermission" />
<!-- Needed to post notifications on devices with Android 13 and later-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".FenixApplication" android:name=".FenixApplication"
android:allowBackup="false" android:allowBackup="false"

@ -31,41 +31,6 @@ object FeatureFlags {
*/ */
const val syncAddressesFeature = true const val syncAddressesFeature = true
/**
* Enables the onboarding sync CFR on the home screen.
*/
const val showSynCFR = true
/**
* Enables the onboarding jump back in CFR on the home screen.
*/
const val showJumpBackInCFR = true
/**
* Enables the first run onboarding updates.
*/
const val showFirstRunOnboardingUpdates = false
/**
* Enables the "recent" tabs feature in the home screen.
*/
const val showRecentTabsFeature = true
/**
* Enables UI features based on history metadata.
*/
const val historyMetadataUIFeature = true
/**
* Enables the recently saved bookmarks feature in the home screen.
*/
const val recentBookmarksFeature = true
/**
* Identifies and separates the tabs list with a secondary section containing least used tabs.
*/
const val inactiveTabs = true
/** /**
* Show Pocket recommended stories on home. * Show Pocket recommended stories on home.
*/ */
@ -82,36 +47,16 @@ object FeatureFlags {
return isPocketRecommendationsFeatureEnabled(context) && Config.channel.isDebug return isPocketRecommendationsFeatureEnabled(context) && Config.channel.isDebug
} }
/**
* Enables showing the homescreen onboarding card.
*/
const val showHomeOnboarding = true
/**
* Enables the Task Continuity enhancements.
*/
const val taskContinuityFeature = true
/** /**
* Enables the Unified Search feature. * Enables the Unified Search feature.
*/ */
val unifiedSearchFeature = Config.channel.isNightlyOrDebug val unifiedSearchFeature = Config.channel.isNightlyOrDebug
/**
* Enables receiving from the messaging framework.
*/
const val messagingFeature = true
/** /**
* Enables compose on the tabs tray items. * Enables compose on the tabs tray items.
*/ */
val composeTabsTray = Config.channel.isDebug val composeTabsTray = Config.channel.isDebug
/**
* Enables the wallpaper onboarding.
*/
const val wallpaperOnboardingEnabled = true
/** /**
* Enables the wallpaper v2 enhancements. * Enables the wallpaper v2 enhancements.
*/ */

@ -50,7 +50,6 @@ import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.facts.register import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.base.observer.Observable
import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.components.support.ktx.android.content.isMainProcess
import mozilla.components.support.ktx.android.content.runOnlyInMainProcess import mozilla.components.support.ktx.android.content.runOnlyInMainProcess
import mozilla.components.support.locale.LocaleAwareApplication import mozilla.components.support.locale.LocaleAwareApplication
@ -59,8 +58,6 @@ import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.utils.logElapsedTime import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidAutofill import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.CustomizeHome import org.mozilla.fenix.GleanMetrics.CustomizeHome
@ -83,6 +80,7 @@ import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.setCustomEndpointIfAvailable import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
@ -134,12 +132,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
return return
} }
// We need to always initialize Glean and do it early here. // DO NOT ADD ANYTHING ABOVE HERE.
initializeGlean()
setupInMainProcessOnly() setupInMainProcessOnly()
// DO NOT ADD ANYTHING UNDER HERE.
downloadWallpapers()
// DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL. // DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL.
val stop = SystemClock.elapsedRealtimeNanos() val stop = SystemClock.elapsedRealtimeNanos()
@ -197,11 +192,22 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@CallSuper @CallSuper
open fun setupInMainProcessOnly() { open fun setupInMainProcessOnly() {
// ⚠️ DO NOT ADD ANYTHING ABOVE THIS LINE.
// Especially references to the engine/BrowserStore which can alter the app initialization.
// See: https://github.com/mozilla-mobile/fenix/issues/26320
//
// We can initialize Nimbus before Glean because Glean will queue messages
// before it's initialized.
initializeNimbus()
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register() ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run { run {
// We need to always initialize Glean and do it early here.
initializeGlean()
// Attention: Do not invoke any code from a-s in this scope. // Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord() val megazordSetup = finishSetupMegazord()
setDayNightTheme() setDayNightTheme()
components.strictMode.enableStrictMode(true) components.strictMode.enableStrictMode(true)
@ -222,6 +228,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
restoreBrowserState() restoreBrowserState()
restoreDownloads() restoreDownloads()
restoreMessaging()
// Just to make sure it is impossible for any application-services pieces // Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization // to invoke parts of itself that require complete megazord initialization
@ -244,6 +251,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initVisualCompletenessQueueAndQueueTasks() initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store)) ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
downloadWallpapers()
} }
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage @OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
@ -359,6 +368,16 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
} }
// For Android 13 or above, prompt the user for notification permission at the start.
// Regardless if the user accepts or denies the permission prompt, the prompt will occur only once.
fun queueNotificationPermissionRequest() {
if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
queue.runIfReadyOrQueue {
ensureMarketingChannelExists(this)
}
}
}
initQueue() initQueue()
// We init these items in the visual completeness queue to avoid them initing in the critical // We init these items in the visual completeness queue to avoid them initing in the critical
@ -368,6 +387,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
queueReviewPrompt() queueReviewPrompt()
queueRestoreLocale() queueRestoreLocale()
queueStorageMaintenance() queueStorageMaintenance()
queueNotificationPermissionRequest()
} }
private fun startMetricsIfEnabled() { private fun startMetricsIfEnabled() {
@ -415,6 +435,15 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
.install(this) .install(this)
} }
protected open fun initializeNimbus() {
beginSetupMegazord()
// This lazily constructs the Nimbus object…
val nimbus = components.analytics.experiments
// … which we then can populate the feature configuration.
FxNimbus.initialize { nimbus }
}
/** /**
* Initiate Megazord sequence! Megazord Battle Mode! * Initiate Megazord sequence! Megazord Battle Mode!
* *
@ -424,54 +453,43 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
* Documentation on what megazords are, and why they're needed: * Documentation on what megazords are, and why they're needed:
* - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md * - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
* - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html * - https://mozilla.github.io/application-services/docs/applications/consuming-megazord-libraries.html
*
* This is the initialization of the megazord without setting up networking, i.e. needing the
* engine for networking. This should do the minimum work necessary as it is done on the main
* thread, early in the app startup sequence.
*/ */
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage private fun beginSetupMegazord() {
private fun setupMegazord(): Deferred<Unit> {
// Note: Megazord.init() must be called as soon as possible ... // Note: Megazord.init() must be called as soon as possible ...
Megazord.init() Megazord.init()
// Give the generated FxNimbus a closure to lazily get the Nimbus object
FxNimbus.initialize { components.analytics.experiments } initializeRustErrors(components.analytics.crashReporter)
// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
// Once application-services has switched to using the new
// error reporting system, RustLog shouldn't input a CrashReporter
// anymore.
// (https://github.com/mozilla/application-services/issues/4981).
RustLog.enable(components.analytics.crashReporter)
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
private fun finishSetupMegazord(): Deferred<Unit> {
return GlobalScope.async(Dispatchers.IO) { return GlobalScope.async(Dispatchers.IO) {
initializeRustErrors(components.analytics.crashReporter) if (Config.channel.isDebug) {
// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. RustHttpConfig.allowEmulatorLoopback()
RustHttpConfig.setClient(lazy { components.core.client })
// Once application-services has switched to using the new
// error reporting system, RustLog shouldn't input a CrashReporter
// anymore.
// (https://github.com/mozilla/application-services/issues/4981).
RustLog.enable(components.analytics.crashReporter)
// We want to ensure Nimbus is initialized as early as possible so we can
// experiment on features close to startup.
// But we need viaduct (the RustHttp client) to be ready before we do.
components.analytics.experiments.apply {
setupNimbusObserver(this)
} }
} RustHttpConfig.setClient(lazy { components.core.client })
}
private fun setupNimbusObserver(nimbus: Observable<NimbusInterface.Observer>) { // Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
nimbus.register( // experiments recipes from the server.
object : NimbusInterface.Observer { components.analytics.experiments.fetchExperiments()
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) { }
onNimbusStartupAndUpdate()
}
},
)
} }
private fun onNimbusStartupAndUpdate() { private fun restoreMessaging() {
// When Nimbus has successfully started up, we can apply our engine settings experiment. if (settings().isExperimentationEnabled) {
// Any previous value that was set on the engine will be overridden from those set in
// Core.Engine.DefaultSettings.
// NOTE ⚠️: Any startup experiment we want to run needs to have it's value re-applied here.
components.core.engine.settings.trackingProtectionPolicy =
components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
val settings = settings()
if (FeatureFlags.messagingFeature && settings.isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore) components.appStore.dispatch(AppAction.MessagingAction.Restore)
} }
reportHomeScreenSectionMetrics(settings)
} }
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {

@ -42,6 +42,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.SearchAction
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
@ -103,6 +104,7 @@ import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDir
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
@ -275,7 +277,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
val safeIntent = intent?.toSafeIntent() val safeIntent = intent?.toSafeIntent()
safeIntent safeIntent
?.let(::getIntentSource) ?.let(::getIntentSource)
?.also { Events.appOpened.record(Events.AppOpenedExtra(it)) } ?.also {
Events.appOpened.record(Events.AppOpenedExtra(it))
// This will record an event in Nimbus' internal event store. Used for behavioral targeting
components.analytics.experiments.recordEvent("app_opened")
}
} }
supportActionBar?.hide() supportActionBar?.hide()
@ -377,7 +383,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.appStore.dispatch(AppAction.ResumedMetricsAction) components.appStore.dispatch(AppAction.ResumedMetricsAction)
DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext) DefaultBrowserNotificationWorker.setDefaultBrowserNotificationIfNeeded(applicationContext)
ReEngagementNotificationWorker.setReEngagementNotificationIfNeeded(applicationContext)
} }
// This was done in order to refresh search engines when app is running in background
// and the user changes the system language
// More details here: https://github.com/mozilla-mobile/fenix/pull/27793#discussion_r1029892536
components.core.store.dispatch(SearchAction.RefreshSearchEnginesAction)
} }
override fun onStart() { override fun onStart() {
@ -618,7 +630,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return return
} }
} }
super.onBackPressed() super.getOnBackPressedDispatcher().onBackPressed()
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

@ -15,6 +15,7 @@ import mozilla.components.feature.intent.ext.sanitize
import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.feature.intent.processing.IntentProcessor
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_CATEGORY
import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE import mozilla.components.support.utils.EXTRA_ACTIVITY_REFERRER_PACKAGE
import mozilla.components.support.utils.ext.getApplicationInfoCompat
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE
import org.mozilla.fenix.components.IntentProcessorType import org.mozilla.fenix.components.IntentProcessorType
@ -125,7 +126,7 @@ class IntentReceiverActivity : Activity() {
// Category is supported for API>=26. // Category is supported for API>=26.
r.host?.let { host -> r.host?.let { host ->
try { try {
val category = packageManager.getApplicationInfo(host, 0).category val category = packageManager.getApplicationInfoCompat(host, 0).category
intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category) intent.putExtra(EXTRA_ACTIVITY_REFERRER_CATEGORY, category)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
// At least we tried. // At least we tried.

@ -411,17 +411,22 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
} }
private fun announceForAccessibility(announcementText: CharSequence) { private fun announceForAccessibility(announcementText: CharSequence) {
val event = AccessibilityEvent.obtain( val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent.TYPE_ANNOUNCEMENT, AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
) } else {
@Suppress("DEPRECATION")
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
}
binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event) binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event)
event.text.add(announcementText) event.text.add(announcementText)
event.contentDescription = null event.contentDescription = null
binding?.addonProgressOverlay?.overlayCardView?.parent?.requestSendAccessibilityEvent( binding?.addonProgressOverlay?.overlayCardView?.let {
binding?.addonProgressOverlay?.overlayCardView, it.parent?.requestSendAccessibilityEvent(
event, it,
) event,
)
}
} }
companion object { companion object {

@ -33,7 +33,6 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -1251,7 +1250,7 @@ abstract class BaseBrowserFragment :
viewLifecycleOwner.lifecycleScope.launch(Main) { viewLifecycleOwner.lifecycleScope.launch(Main) {
val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin -> val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin ->
val storage = requireComponents.core.permissionStorage val storage = requireComponents.core.permissionStorage
storage.findSitePermissionsBy(origin) storage.findSitePermissionsBy(origin, tab.content.private)
} }
view?.let { view?.let {
@ -1370,6 +1369,7 @@ abstract class BaseBrowserFragment :
.setText(getString(R.string.full_screen_notification)) .setText(getString(R.string.full_screen_notification))
.show() .show()
activity?.enterToImmersiveMode() activity?.enterToImmersiveMode()
(view as? SwipeGestureLayout)?.isSwipeEnabled = false
browserToolbarView.collapse() browserToolbarView.collapse()
browserToolbarView.view.isVisible = false browserToolbarView.view.isVisible = false
val browserEngine = binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams val browserEngine = binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
@ -1384,6 +1384,7 @@ abstract class BaseBrowserFragment :
MediaState.fullscreen.record(NoExtras()) MediaState.fullscreen.record(NoExtras())
} else { } else {
activity?.exitImmersiveMode() activity?.exitImmersiveMode()
(view as? SwipeGestureLayout)?.isSwipeEnabled = true
(activity as? HomeActivity)?.let { activity -> (activity as? HomeActivity)?.let { activity ->
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
} }
@ -1398,6 +1399,11 @@ abstract class BaseBrowserFragment :
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen) binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen)
} }
@CallSuper
internal open fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) {
toolbar.dismissMenu()
}
/* /*
* Dereference these views when the fragment view is destroyed to prevent memory leaks * Dereference these views when the fragment view is destroyed to prevent memory leaks
*/ */
@ -1470,7 +1476,9 @@ abstract class BaseBrowserFragment :
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
_browserToolbarView?.dismissMenu() _browserToolbarView?.let {
onUpdateToolbarForConfigurationChange(it)
}
} }
// This method is called in response to native web extension messages from // This method is called in response to native web extension messages from

@ -11,8 +11,12 @@ import android.view.ViewGroup
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
@ -31,12 +35,14 @@ import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.shortcut.PwaOnboardingObserver import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
@ -52,6 +58,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private var readerModeAvailable = false private var readerModeAvailable = false
private var pwaOnboardingObserver: PwaOnboardingObserver? = null private var pwaOnboardingObserver: PwaOnboardingObserver? = null
private var forwardAction: BrowserToolbar.TwoStateButton? = null
private var backAction: BrowserToolbar.TwoStateButton? = null
private var refreshAction: BrowserToolbar.TwoStateButton? = null
private var isTablet: Boolean = false
@Suppress("LongMethod") @Suppress("LongMethod")
override fun initializeUI(view: View, tab: SessionState) { override fun initializeUI(view: View, tab: SessionState) {
super.initializeUI(view, tab) super.initializeUI(view, tab)
@ -84,86 +95,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
browserToolbarView.view.addNavigationAction(homeAction) browserToolbarView.view.addNavigationAction(homeAction)
if (resources.getBoolean(R.bool.tablet)) { updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet))
val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
val disableTint = ThemeManager.resolveAttribute(R.attr.textDisabled, context)
val backAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_back,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoBack ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = true),
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = false),
)
},
)
browserToolbarView.view.addNavigationAction(backAction)
val forwardAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_forward,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoForward ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = true),
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = false),
)
},
)
browserToolbarView.view.addNavigationAction(forwardAction)
val refreshAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_refresh,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = enableTint,
isInPrimaryState = {
getCurrentTab()?.content?.loading == false
},
secondaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_stop,
)!!,
secondaryContentDescription = context.getString(R.string.browser_menu_stop),
disableInSecondaryState = false,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = true),
)
},
listener = {
if (getCurrentTab()?.content?.loading == true) {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(ToolbarMenu.Item.Stop)
} else {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = false),
)
}
},
)
browserToolbarView.view.addNavigationAction(refreshAction)
}
val readerModeAction = val readerModeAction =
BrowserToolbar.ToggleButton( BrowserToolbar.ToggleButton(
@ -243,6 +175,141 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
} }
override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) {
super.onUpdateToolbarForConfigurationChange(toolbar)
updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet))
}
@VisibleForTesting
internal fun updateToolbarActions(isTablet: Boolean) {
if (isTablet == this.isTablet) return
if (isTablet) {
addTabletActions(requireContext())
} else {
removeTabletActions()
}
this.isTablet = isTablet
}
@Suppress("LongMethod")
private fun addTabletActions(context: Context) {
val enableTint = ThemeManager.resolveAttribute(R.attr.textPrimary, context)
val disableTint = ThemeManager.resolveAttribute(R.attr.textDisabled, context)
if (backAction == null) {
backAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_back,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoBack ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = true),
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = false),
)
},
)
}
backAction?.let {
browserToolbarView.view.addNavigationAction(it)
}
if (forwardAction == null) {
forwardAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_forward,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoForward ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = true),
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = false),
)
},
)
}
forwardAction?.let {
browserToolbarView.view.addNavigationAction(it)
}
if (refreshAction == null) {
refreshAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_refresh,
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = enableTint,
isInPrimaryState = {
getCurrentTab()?.content?.loading == false
},
secondaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_stop,
)!!,
secondaryContentDescription = context.getString(R.string.browser_menu_stop),
disableInSecondaryState = false,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = true),
)
},
listener = {
if (getCurrentTab()?.content?.loading == true) {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(ToolbarMenu.Item.Stop)
} else {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = false),
)
}
},
)
}
refreshAction?.let {
browserToolbarView.view.addNavigationAction(it)
}
browserToolbarView.view.invalidateActions()
}
private fun removeTabletActions() {
forwardAction?.let {
browserToolbarView.view.removeNavigationAction(it)
}
backAction?.let {
browserToolbarView.view.removeNavigationAction(it)
}
refreshAction?.let {
browserToolbarView.view.removeNavigationAction(it)
}
browserToolbarView.view.invalidateActions()
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
val context = requireContext() val context = requireContext()
@ -261,6 +328,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
subscribeToTabCollections() subscribeToTabCollections()
updateLastBrowseActivity()
} }
override fun onStop() { override fun onStop() {
@ -297,22 +365,35 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> val useCase = requireComponents.useCases.trackingProtectionUseCases
runIfFragmentIsAttached { FxNimbus.features.cookieBanners.recordExposure()
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains useCase.containsException(tab.id) { hasTrackingProtectionException ->
val directions = lifecycleScope.launch(Dispatchers.Main) {
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( val cookieBannersStorage = requireComponents.core.cookieBannersStorage
sessionId = tab.id, val hasCookieBannerException = withContext(Dispatchers.IO) {
url = tab.content.url, cookieBannersStorage.hasException(
title = tab.content.title, tab.content.url,
isSecured = tab.content.securityInfo.secure, tab.content.private,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
) )
nav(R.id.browserFragment, directions) }
runIfFragmentIsAttached {
val isTrackingProtectionEnabled =
tab.trackingProtection.enabled && !hasTrackingProtectionException
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled,
isCookieHandlingEnabled = !hasCookieBannerException,
)
nav(R.id.browserFragment, directions)
}
} }
} }
} }

@ -56,19 +56,24 @@ class SwipeGestureLayout @JvmOverloads constructor(
defStyleAttr: Int = 0, defStyleAttr: Int = 0,
) : FrameLayout(context, attrs, defStyleAttr) { ) : FrameLayout(context, attrs, defStyleAttr) {
/**
* Controls whether the swiping functionality is active or not.
*/
var isSwipeEnabled = true
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() { private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean { override fun onDown(e: MotionEvent): Boolean {
return true return true
} }
override fun onScroll( override fun onScroll(
e1: MotionEvent?, e1: MotionEvent,
e2: MotionEvent?, e2: MotionEvent,
distanceX: Float, distanceX: Float,
distanceY: Float, distanceY: Float,
): Boolean { ): Boolean {
val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false val start = e1.let { event -> PointF(event.rawX, event.rawY) }
val next = e2?.let { event -> PointF(event.rawX, event.rawY) } ?: return false val next = e2.let { event -> PointF(event.rawX, event.rawY) }
if (activeListener == null && !handledInitialScroll) { if (activeListener == null && !handledInitialScroll) {
activeListener = listeners.firstOrNull { listener -> activeListener = listeners.firstOrNull { listener ->
@ -81,8 +86,8 @@ class SwipeGestureLayout @JvmOverloads constructor(
} }
override fun onFling( override fun onFling(
e1: MotionEvent?, e1: MotionEvent,
e2: MotionEvent?, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float, velocityY: Float,
): Boolean { ): Boolean {
@ -107,6 +112,10 @@ class SwipeGestureLayout @JvmOverloads constructor(
} }
override fun onInterceptTouchEvent(event: MotionEvent): Boolean { override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
if (!isSwipeEnabled) {
return false
}
return when (event.actionMasked) { return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> { MotionEvent.ACTION_DOWN -> {
handledInitialScroll = false handledInitialScroll = false

@ -258,7 +258,7 @@ class ToolbarGestureHandler(
.setDuration(shortAnimationDuration.toLong()) .setDuration(shortAnimationDuration.toLong())
.setListener( .setListener(
object : AnimatorListenerAdapter() { object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator?) { override fun onAnimationEnd(animation: Animator) {
tabPreview.isVisible = false tabPreview.isVisible = false
} }
}, },

@ -13,8 +13,8 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.toShortUrl
class CollectionCreationStore( class CollectionCreationStore(
initialState: CollectionCreationState, initialState: CollectionCreationState,

@ -20,12 +20,12 @@ import androidx.transition.TransitionManager
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.ComponentCollectionCreationBinding import org.mozilla.fenix.databinding.ComponentCollectionCreationBinding
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl
class CollectionCreationView( class CollectionCreationView(
private val container: ViewGroup, private val container: ViewGroup,

@ -46,7 +46,6 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.sync.SyncedTabsIntegration import org.mozilla.fenix.sync.SyncedTabsIntegration
import org.mozilla.fenix.utils.Settings
/** /**
* Component group for background services. These are the components that need to be accessed from within a * Component group for background services. These are the components that need to be accessed from within a
@ -127,7 +126,7 @@ class BackgroundServices(
} }
private val telemetryAccountObserver = TelemetryAccountObserver( private val telemetryAccountObserver = TelemetryAccountObserver(
context.settings(), context,
) )
val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode) val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode)
@ -219,13 +218,16 @@ private class AccountManagerReadyObserver(
@VisibleForTesting(otherwise = PRIVATE) @VisibleForTesting(otherwise = PRIVATE)
internal class TelemetryAccountObserver( internal class TelemetryAccountObserver(
private val settings: Settings, private val context: Context,
) : AccountObserver { ) : AccountObserver {
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) { override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
settings.signedInFxaAccount = true context.settings().signedInFxaAccount = true
when (authType) { when (authType) {
// User signed-in into an existing FxA account. // User signed-in into an existing FxA account.
AuthType.Signin -> SyncAuth.signIn.record(NoExtras()) AuthType.Signin -> {
SyncAuth.signIn.record(NoExtras())
context.components.analytics.experiments.recordEvent("sync_auth.sign_in")
}
// User created a new FxA account. // User created a new FxA account.
AuthType.Signup -> SyncAuth.signUp.record(NoExtras()) AuthType.Signup -> SyncAuth.signUp.record(NoExtras())
@ -254,6 +256,6 @@ internal class TelemetryAccountObserver(
override fun onLoggedOut() { override fun onLoggedOut() {
SyncAuth.signOut.record(NoExtras()) SyncAuth.signOut.record(NoExtras())
settings.signedInFxaAccount = false context.settings().signedInFxaAccount = false
} }
} }

@ -12,6 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources.getDrawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.cookiebanners.GeckoCookieBannersStorage
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage import mozilla.components.browser.engine.gecko.permission.GeckoSitePermissionsStorage
import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.BrowserIcons
@ -132,6 +133,8 @@ class Core(
R.color.fx_mobile_layer_color_1, R.color.fx_mobile_layer_color_1,
), ),
httpsOnlyMode = context.settings().getHttpsOnlyMode(), httpsOnlyMode = context.settings().getHttpsOnlyMode(),
cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandling(),
cookieBannerHandlingMode = context.settings().getCookieBannerHandling(),
) )
GeckoEngine( GeckoEngine(
@ -181,6 +184,8 @@ class Core(
) )
} }
val cookieBannersStorage by lazyMonitored { GeckoCookieBannersStorage(geckoRuntime) }
val geckoSitePermissionsStorage by lazyMonitored { val geckoSitePermissionsStorage by lazyMonitored {
GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context)) GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context))
} }

@ -20,26 +20,58 @@ class PermissionStorage(
context.components.core.geckoSitePermissionsStorage, context.components.core.geckoSitePermissionsStorage,
) { ) {
/**
* Persists the [sitePermissions] provided as a parameter.
* @param sitePermissions the [sitePermissions] to be stored.
*/
suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) { suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.save(sitePermissions) permissionsStorage.save(sitePermissions, private = false)
} }
suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(dispatcher) { /**
permissionsStorage.findSitePermissionsBy(origin) * Finds all SitePermissions that match the [origin].
} * @param origin the site to be used as filter in the search.
* @param private indicates if the [origin] belongs to a private session.
*/
suspend fun findSitePermissionsBy(origin: String, private: Boolean): SitePermissions? =
withContext(dispatcher) {
permissionsStorage.findSitePermissionsBy(origin, private = private)
}
suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) { /**
permissionsStorage.update(sitePermissions) * Replaces an existing SitePermissions with the values of [sitePermissions] provided as a parameter.
} * @param sitePermissions the sitePermissions to be updated.
* @param private indicates if the [SitePermissions] belongs to a private session.
*/
suspend fun updateSitePermissions(sitePermissions: SitePermissions, private: Boolean) =
withContext(dispatcher) {
permissionsStorage.update(sitePermissions, private = private)
}
/**
* Returns all saved [SitePermissions] instances as a [DataSource.Factory].
*
* A consuming app can transform the data source into a `LiveData<PagedList>` of when using RxJava2 into a
* `Flowable<PagedList>` or `Observable<PagedList>`, that can be observed.
*
* - https://developer.android.com/topic/libraries/architecture/paging/data
* - https://developer.android.com/topic/libraries/architecture/paging/ui
*/
suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> { suspend fun getSitePermissionsPaged(): DataSource.Factory<Int, SitePermissions> {
return permissionsStorage.getSitePermissionsPaged() return permissionsStorage.getSitePermissionsPaged()
} }
/**
* Deletes all sitePermissions that match the sitePermissions provided as a parameter.
* @param sitePermissions the sitePermissions to be deleted from the storage.
*/
suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) { suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.remove(sitePermissions) permissionsStorage.remove(sitePermissions, private = false)
} }
/**
* Deletes all sitePermissions sitePermissions.
*/
suspend fun deleteAllSitePermissions() = withContext(dispatcher) { suspend fun deleteAllSitePermissions() = withContext(dispatcher) {
permissionsStorage.removeAll() permissionsStorage.removeAll()
} }

@ -19,8 +19,8 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage import mozilla.components.feature.tab.collections.TabCollectionStorage
import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry import mozilla.components.support.base.observer.ObserverRegistry
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
private const val COLLECTION_MAX_TITLE_LENGTH = 20 private const val COLLECTION_MAX_TITLE_LENGTH = 20

@ -23,14 +23,9 @@ sealed class Event {
object SetAsDefault : GrowthData("xgpcgt") object SetAsDefault : GrowthData("xgpcgt")
/** /**
* Event recording the first time Firefox has been resumed in a 24 hour period. * Event recording that an ad was clicked in a search engine results page.
*/ */
object FirstAppOpenForDay : GrowthData("41hl22") object SerpAdClicked : GrowthData("e2x17e")
/**
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
*/
object FirstUriLoadForDay : GrowthData("ja86ek")
/** /**
* Event recording the first time Firefox is used 3 days in a row in the first week of install. * Event recording the first time Firefox is used 3 days in a row in the first week of install.

@ -262,6 +262,7 @@ internal class ReleaseMetricController(
Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> { Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> {
BrowserSearch.adClicks[value!!].add() BrowserSearch.adClicks[value!!].add()
track(Event.GrowthData.SerpAdClicked)
} }
Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> { Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> {
BrowserSearch.withAds[value!!].add() BrowserSearch.withAds[value!!].add()

@ -1,3 +1,7 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
@ -23,7 +27,6 @@ class MetricsMiddleware(
private fun handleAction(action: AppAction) = when (action) { private fun handleAction(action: AppAction) = when (action) {
is AppAction.ResumedMetricsAction -> { is AppAction.ResumedMetricsAction -> {
metrics.track(Event.GrowthData.SetAsDefault) metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstAppOpenForDay)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity) metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
} }
else -> Unit else -> Unit

@ -8,6 +8,7 @@ import android.content.Context
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -46,21 +47,20 @@ internal class DefaultMetricsStorage(
*/ */
override suspend fun shouldTrack(event: Event): Boolean = override suspend fun shouldTrack(event: Event): Boolean =
withContext(dispatcher) { withContext(dispatcher) {
// The side-effect of storing days of use needs to happen during the first two days after // The side-effect of storing days of use always needs to happen.
// install, which would normally be skipped by shouldSendGenerally.
updateDaysOfUse() updateDaysOfUse()
val currentTime = System.currentTimeMillis()
shouldSendGenerally() && when (event) { shouldSendGenerally() && when (event) {
Event.GrowthData.SetAsDefault -> { Event.GrowthData.SetAsDefault -> {
!settings.setAsDefaultGrowthSent && checkDefaultBrowser() currentTime.duringFirstMonth() &&
} !settings.setAsDefaultGrowthSent &&
Event.GrowthData.FirstAppOpenForDay -> { checkDefaultBrowser()
settings.resumeGrowthLastSent.hasBeenMoreThanDaySince()
}
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent.hasBeenMoreThanDaySince()
} }
Event.GrowthData.FirstWeekSeriesActivity -> { Event.GrowthData.FirstWeekSeriesActivity -> {
shouldTrackFirstWeekActivity() currentTime.duringFirstMonth() && shouldTrackFirstWeekActivity()
}
Event.GrowthData.SerpAdClicked -> {
currentTime.duringFirstMonth() && !settings.adClickGrowthSent
} }
} }
} }
@ -70,15 +70,12 @@ internal class DefaultMetricsStorage(
Event.GrowthData.SetAsDefault -> { Event.GrowthData.SetAsDefault -> {
settings.setAsDefaultGrowthSent = true settings.setAsDefaultGrowthSent = true
} }
Event.GrowthData.FirstAppOpenForDay -> {
settings.resumeGrowthLastSent = System.currentTimeMillis()
}
Event.GrowthData.FirstUriLoadForDay -> {
settings.uriLoadGrowthLastSent = System.currentTimeMillis()
}
Event.GrowthData.FirstWeekSeriesActivity -> { Event.GrowthData.FirstWeekSeriesActivity -> {
settings.firstWeekSeriesGrowthSent = true settings.firstWeekSeriesGrowthSent = true
} }
Event.GrowthData.SerpAdClicked -> {
settings.adClickGrowthSent = true
}
} }
} }
@ -86,13 +83,13 @@ internal class DefaultMetricsStorage(
val daysOfUse = settings.firstWeekDaysOfUseGrowthData val daysOfUse = settings.firstWeekDaysOfUseGrowthData
val currentDate = Calendar.getInstance(Locale.US) val currentDate = Calendar.getInstance(Locale.US)
val currentDateString = dateFormatter.format(currentDate.time) val currentDateString = dateFormatter.format(currentDate.time)
if (currentDate.timeInMillis.withinFirstWeek() && daysOfUse.none { it == currentDateString }) { if (currentDate.timeInMillis.duringFirstWeek() && daysOfUse.none { it == currentDateString }) {
settings.firstWeekDaysOfUseGrowthData = daysOfUse + currentDateString settings.firstWeekDaysOfUseGrowthData = daysOfUse + currentDateString
} }
} }
private fun shouldTrackFirstWeekActivity(): Boolean = Result.runCatching { private fun shouldTrackFirstWeekActivity(): Boolean = Result.runCatching {
if (!System.currentTimeMillis().withinFirstWeek() || settings.firstWeekSeriesGrowthSent) { if (!System.currentTimeMillis().duringFirstWeek() || settings.firstWeekSeriesGrowthSent) {
return false return false
} }
@ -120,14 +117,13 @@ internal class DefaultMetricsStorage(
return false return false
}.getOrDefault(false) }.getOrDefault(false)
private fun Long.hasBeenMoreThanDaySince(): Boolean =
System.currentTimeMillis() - this > dayMillis
private fun Long.toCalendar(): Calendar = Calendar.getInstance(Locale.US).also { calendar -> private fun Long.toCalendar(): Calendar = Calendar.getInstance(Locale.US).also { calendar ->
calendar.timeInMillis = this calendar.timeInMillis = this
} }
private fun Long.withinFirstWeek() = this < getInstalledTime() + fullWeekMillis private fun Long.duringFirstWeek() = this < getInstalledTime() + fullWeekMillis
private fun Long.duringFirstMonth() = this < getInstalledTime() + shortestMonthMillis
private fun Calendar.createNextDay() = (this.clone() as Calendar).also { calendar -> private fun Calendar.createNextDay() = (this.clone() as Calendar).also { calendar ->
calendar.add(Calendar.DAY_OF_MONTH, 1) calendar.add(Calendar.DAY_OF_MONTH, 1)
@ -135,8 +131,7 @@ internal class DefaultMetricsStorage(
companion object { companion object {
private const val dayMillis: Long = 1000 * 60 * 60 * 24 private const val dayMillis: Long = 1000 * 60 * 60 * 24
private const val windowStartMillis: Long = dayMillis * 2 private const val shortestMonthMillis: Long = dayMillis * 28
private const val windowEndMillis: Long = dayMillis * 28
// Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length // Note this is 8 so that recording of FirstWeekSeriesActivity happens throughout the length
// of the 7th day after install // of the 7th day after install
@ -145,21 +140,15 @@ internal class DefaultMetricsStorage(
/** /**
* Determines whether events should be tracked based on some general criteria: * Determines whether events should be tracked based on some general criteria:
* - user has installed as a result of a campaign * - user has installed as a result of a campaign
* - user is within 2-28 days of install
* - tracking is still enabled through Nimbus * - tracking is still enabled through Nimbus
*/ */
fun shouldSendGenerally(context: Context): Boolean { fun shouldSendGenerally(context: Context): Boolean {
val installedTime = getInstalledTime(context)
val timeDifference = System.currentTimeMillis() - installedTime
val withinWindow = timeDifference in windowStartMillis..windowEndMillis
return context.settings().adjustCampaignId.isNotEmpty() && return context.settings().adjustCampaignId.isNotEmpty() &&
FxNimbus.features.growthData.value().enabled && FxNimbus.features.growthData.value().enabled
withinWindow
} }
fun getInstalledTime(context: Context): Long = context.packageManager fun getInstalledTime(context: Context): Long = context.packageManager
.getPackageInfo(context.packageName, 0) .getPackageInfoCompat(context.packageName, 0)
.firstInstallTime .firstInstallTime
} }
} }

@ -6,6 +6,7 @@ package org.mozilla.fenix.components.metrics
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
object MozillaProductDetector { object MozillaProductDetector {
@ -45,7 +46,7 @@ object MozillaProductDetector {
fun packageIsInstalled(context: Context, packageName: String): Boolean { fun packageIsInstalled(context: Context, packageName: String): Boolean {
try { try {
context.packageManager.getPackageInfo(packageName, 0) context.packageManager.getPackageInfoCompat(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) { } catch (e: PackageManager.NameNotFoundException) {
return false return false
} }

@ -22,7 +22,7 @@ import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.support.utils.URLStringUtils import mozilla.components.support.ktx.util.URLStringUtils
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration

@ -49,11 +49,11 @@ class MenuPresenter(
menuToolbar.invalidateActions() menuToolbar.invalidateActions()
} }
override fun onViewDetachedFromWindow(v: View?) { override fun onViewDetachedFromWindow(v: View) {
menuToolbar.onStop() menuToolbar.onStop()
} }
override fun onViewAttachedToWindow(v: View?) { override fun onViewAttachedToWindow(v: View) {
// no-op // no-op
} }
} }

@ -17,7 +17,6 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* [Text] containing a substring styled as an URL informing when this is clicked. * [Text] containing a substring styled as an URL informing when this is clicked.
@ -95,7 +94,7 @@ fun ClickableSubstringLink(
private fun ClickableSubstringTextPreview() { private fun ClickableSubstringTextPreview() {
val text = "This text contains a link" val text = "This text contains a link"
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) { Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
ClickableSubstringLink( ClickableSubstringLink(
text = text, text = text,

@ -24,7 +24,6 @@ import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon import mozilla.components.browser.icons.compose.WithIcon
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Load and display the favicon of a particular website. * Load and display the favicon of a particular website.
@ -98,7 +97,7 @@ private fun FaviconPlaceholder(
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun FaviconPreview() { private fun FaviconPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
Favicon( Favicon(
url = "www.mozilla.com", url = "www.mozilla.com",

@ -26,7 +26,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Default layout of a large tab shown in a list taking String arguments for title and caption. * Default layout of a large tab shown in a list taking String arguments for title and caption.
@ -171,7 +170,7 @@ fun ListItemTabSurface(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ListItemTabLargePreview() { private fun ListItemTabLargePreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
ListItemTabLarge( ListItemTabLarge(
imageUrl = "", imageUrl = "",
title = "This is a very long title for a tab but needs to be so for this preview", title = "This is a very long title for a tab but needs to be so for this preview",
@ -184,7 +183,7 @@ private fun ListItemTabLargePreview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ListItemTabSurfacePreview() { private fun ListItemTabSurfacePreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
ListItemTabSurface( ListItemTabSurface(
imageUrl = "", imageUrl = "",
) { ) {
@ -201,7 +200,7 @@ private fun ListItemTabSurfacePreview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ListItemTabSurfaceWithCustomBackgroundPreview() { private fun ListItemTabSurfaceWithCustomBackgroundPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
ListItemTabSurface( ListItemTabSurface(
imageUrl = "", imageUrl = "",
backgroundColor = Color.Cyan, backgroundColor = Color.Cyan,

@ -22,7 +22,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text. * Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text.
@ -74,7 +73,7 @@ fun ListItemTabLargePlaceholder(
@Composable @Composable
@Preview @Preview
private fun ListItemTabLargePlaceholderPreview() { private fun ListItemTabLargePlaceholderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
ListItemTabLargePlaceholder(text = "Item placeholder") ListItemTabLargePlaceholder(text = "Item placeholder")
} }
} }

@ -26,7 +26,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Default layout of a selectable chip. * Default layout of a selectable chip.
@ -78,7 +77,7 @@ fun SelectableChip(
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)
private fun SelectableChipPreview() { private fun SelectableChipPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -95,7 +94,7 @@ private fun SelectableChipPreview() {
@Preview(uiMode = UI_MODE_NIGHT_YES) @Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO) @Preview(uiMode = UI_MODE_NIGHT_NO)
private fun SelectableChipWithCustomColorsPreview() { private fun SelectableChipWithCustomColorsPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Displays a list of items as a staggered horizontal grid placing them on ltr rows and continuing * Displays a list of items as a staggered horizontal grid placing them on ltr rows and continuing
@ -121,7 +120,7 @@ fun StaggeredHorizontalGrid(
@Composable @Composable
@Preview @Preview
private fun StaggeredHorizontalGridPreview() { private fun StaggeredHorizontalGridPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) { Box(Modifier.background(FirefoxTheme.colors.layer2)) {
StaggeredHorizontalGrid( StaggeredHorizontalGrid(
horizontalItemsSpacing = 8.dp, horizontalItemsSpacing = 8.dp,

@ -16,7 +16,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Special caption text for a tab layout shown on one line. * Special caption text for a tab layout shown on one line.
@ -106,7 +105,7 @@ fun TabSubtitleWithInterdot(
@Composable @Composable
@Preview @Preview
private fun TabSubtitleWithInterdotPreview() { private fun TabSubtitleWithInterdotPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) { Box(Modifier.background(FirefoxTheme.colors.layer2)) {
TabSubtitleWithInterdot( TabSubtitleWithInterdot(
firstText = "firstText", firstText = "firstText",

@ -34,7 +34,6 @@ import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon * Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon
@ -138,7 +137,7 @@ private fun ThumbnailImage(
@Preview @Preview
@Composable @Composable
private fun ThumbnailCardPreview() { private fun ThumbnailCardPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
ThumbnailCard( ThumbnailCard(
url = "https://mozilla.com", url = "https://mozilla.com",
key = "123", key = "123",

@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Base component for buttons. * Base component for buttons.
@ -187,7 +186,7 @@ fun DestructiveButton(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ButtonPreview() { private fun ButtonPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Column( Column(
modifier = Modifier modifier = Modifier
.background(FirefoxTheme.colors.layer1) .background(FirefoxTheme.colors.layer1)

@ -13,7 +13,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import java.util.Locale import java.util.Locale
/** /**
@ -48,7 +47,7 @@ fun TextButton(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TextButtonPreview() { private fun TextButtonPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
TextButton( TextButton(
text = "label", text = "label",

@ -28,7 +28,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.Wallpaper import org.mozilla.fenix.wallpapers.Wallpaper
/** /**
@ -124,7 +123,7 @@ private fun HomeSectionHeaderContent(
@Composable @Composable
@Preview @Preview
private fun HomeSectionsHeaderPreview() { private fun HomeSectionsHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
HomeSectionHeader( HomeSectionHeader(
headerText = stringResource(R.string.recently_saved_title), headerText = stringResource(R.string.recently_saved_title),
description = stringResource(R.string.recently_saved_show_all_content_description_2), description = stringResource(R.string.recently_saved_show_all_content_description_2),

@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Expandable header for sections of lists * Expandable header for sections of lists
@ -96,7 +95,7 @@ fun ExpandableListHeader(
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextOnlyHeaderPreview() { private fun TextOnlyHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(headerText = "Section title") ExpandableListHeader(headerText = "Section title")
} }
@ -106,7 +105,7 @@ private fun TextOnlyHeaderPreview() {
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun CollapsibleHeaderPreview() { private fun CollapsibleHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader( ExpandableListHeader(
headerText = "Collapsible section title", headerText = "Collapsible section title",
@ -122,7 +121,7 @@ private fun CollapsibleHeaderPreview() {
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HeaderWithClickableIconPreview() { private fun HeaderWithClickableIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(headerText = "Section title") { ExpandableListHeader(headerText = "Section title") {
Box( Box(
@ -145,7 +144,7 @@ private fun HeaderWithClickableIconPreview() {
@Composable @Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun CollapsibleHeaderWithClickableIconPreview() { private fun CollapsibleHeaderWithClickableIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader( ExpandableListHeader(
headerText = "Section title", headerText = "Section title",

@ -27,7 +27,6 @@ import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Favicon import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
private val LIST_ITEM_HEIGHT = 56.dp private val LIST_ITEM_HEIGHT = 56.dp
@ -250,7 +249,7 @@ private fun ListItem(
@Composable @Composable
@Preview(name = "TextListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "TextListItem", uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextListItemPreview() { private fun TextListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
TextListItem(label = "Label only") TextListItem(label = "Label only")
} }
@ -260,7 +259,7 @@ private fun TextListItemPreview() {
@Composable @Composable
@Preview(name = "TextListItem with a description", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "TextListItem with a description", uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextListItemWithDescriptionPreview() { private fun TextListItemWithDescriptionPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
TextListItem( TextListItem(
label = "Label + description", label = "Label + description",
@ -273,7 +272,7 @@ private fun TextListItemWithDescriptionPreview() {
@Composable @Composable
@Preview(name = "TextListItem with a right icon", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "TextListItem with a right icon", uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextListItemWithIconPreview() { private fun TextListItemWithIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
TextListItem( TextListItem(
label = "Label + right icon", label = "Label + right icon",
@ -288,7 +287,7 @@ private fun TextListItemWithIconPreview() {
@Composable @Composable
@Preview(name = "IconListItem", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "IconListItem", uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun IconListItemPreview() { private fun IconListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
IconListItem( IconListItem(
label = "Left icon list item", label = "Left icon list item",
@ -305,7 +304,7 @@ private fun IconListItemPreview() {
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
) )
private fun IconListItemWithRightIconPreview() { private fun IconListItemWithRightIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
IconListItem( IconListItem(
label = "Left icon list item + right icon", label = "Left icon list item + right icon",
@ -325,7 +324,7 @@ private fun IconListItemWithRightIconPreview() {
uiMode = Configuration.UI_MODE_NIGHT_YES, uiMode = Configuration.UI_MODE_NIGHT_YES,
) )
private fun FaviconListItemPreview() { private fun FaviconListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) { Box(Modifier.background(FirefoxTheme.colors.layer1)) {
FaviconListItem( FaviconListItem(
label = "Favicon + right icon + clicks", label = "Favicon + right icon + clicks",

@ -22,7 +22,6 @@ import mozilla.components.browser.state.state.createTab
import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState import mozilla.components.concept.engine.mediasession.MediaSession.PlaybackState
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Controller buttons for the media (play/pause) state for the given [tab]. * Controller buttons for the media (play/pause) state for the given [tab].
@ -59,7 +58,7 @@ fun MediaImage(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ImagePreview() { private fun ImagePreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
MediaImage( MediaImage(
tab = createTab(url = "https://mozilla.com"), tab = createTab(url = "https://mozilla.com"),
onMediaIconClicked = {}, onMediaIconClicked = {},

@ -34,7 +34,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ThumbnailCard import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* List item used to display a tab that supports clicks, * List item used to display a tab that supports clicks,
@ -172,7 +171,7 @@ private fun Thumbnail(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TabListItemPreview() { private fun TabListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
TabListItem( TabListItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"), tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
onCloseClick = {}, onCloseClick = {},
@ -187,7 +186,7 @@ private fun TabListItemPreview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun SelectedTabListItemPreview() { private fun SelectedTabListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
TabListItem( TabListItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"), tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
onCloseClick = {}, onCloseClick = {},

@ -9,7 +9,11 @@ import android.content.Intent
import android.view.View import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.WebAppManifestParser
@ -29,6 +33,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BaseBrowserFragment import org.mozilla.fenix.browser.BaseBrowserFragment
import org.mozilla.fenix.browser.CustomTabContextMenuCandidate import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
@ -159,21 +164,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
} }
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains -> requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached { lifecycleScope.launch(Dispatchers.IO) {
val directions = ExternalAppBrowserFragmentDirections val hasException =
.actionGlobalQuickSettingsSheetDialogFragment( cookieBannersStorage.hasException(tab.content.url, tab.content.private)
sessionId = tab.id, withContext(Dispatchers.Main) {
url = tab.content.url, runIfFragmentIsAttached {
title = tab.content.title, val directions = ExternalAppBrowserFragmentDirections
isSecured = tab.content.securityInfo.secure, .actionGlobalQuickSettingsSheetDialogFragment(
sitePermissions = sitePermissions, sessionId = tab.id,
gravity = getAppropriateLayoutGravity(), url = tab.content.url,
certificateName = tab.content.securityInfo.issuer, title = tab.content.title,
permissionHighlights = tab.content.permissionHighlights, isSecured = tab.content.securityInfo.secure,
isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains, sitePermissions = sitePermissions,
) gravity = getAppropriateLayoutGravity(),
nav(R.id.externalAppBrowserFragment, directions) certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains,
isCookieHandlingEnabled = !hasException,
)
nav(R.id.externalAppBrowserFragment, directions)
}
}
} }
} }
} }

@ -5,36 +5,17 @@
package org.mozilla.fenix.experiments package org.mozilla.fenix.experiments
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.StrictMode
import mozilla.components.service.nimbus.Nimbus
import mozilla.components.service.nimbus.NimbusApi import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.NimbusAppInfo import mozilla.components.service.nimbus.NimbusAppInfo
import mozilla.components.service.nimbus.NimbusDisabled import mozilla.components.service.nimbus.NimbusBuilder
import mozilla.components.service.nimbus.NimbusServerSettings
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.NimbusInterface import org.json.JSONObject
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.mozilla.experiments.nimbus.internal.NimbusException import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.experiments.nimbus.joinOrTimeout
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.runBlockingIncrement
/**
* Fenix specific observer of Nimbus events.
*
* The generated code `FxNimbus` provides a cache which should be invalidated
* when the experiments recipes are updated.
*/
private val observer = object : NimbusInterface.Observer {
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
FxNimbus.invalidateCachedValues()
}
}
/** /**
* The maximum amount of time the app launch will be blocked to load experiments from disk. * The maximum amount of time the app launch will be blocked to load experiments from disk.
@ -44,93 +25,59 @@ private val observer = object : NimbusInterface.Observer {
*/ */
private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L
@Suppress("TooGenericExceptionCaught") /**
fun createNimbus(context: Context, url: String?): NimbusApi { * Create the Nimbus singleton object for the Fenix app.
val errorReporter: ((String, Throwable) -> Unit) = reporter@{ message, e -> */
Logger.error("Nimbus error: $message", e) fun createNimbus(context: Context, urlString: String?): NimbusApi {
val isAppFirstRun = context.settings().isFirstNimbusRun
if (e is NimbusException && !e.isReportableError()) { if (isAppFirstRun) {
return@reporter context.settings().isFirstNimbusRun = false
}
context.components.analytics.crashReporter.submitCaughtException(e)
} }
return try {
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
// but we keep this here to not mix feature flags and how we configure Nimbus.
val serverSettings = if (!url.isNullOrBlank()) {
if (context.settings().nimbusUsePreview) {
NimbusServerSettings(url = Uri.parse(url), collection = "nimbus-preview")
} else {
NimbusServerSettings(url = Uri.parse(url))
}
} else {
null
}
// Global opt out state is stored in Nimbus, and shouldn't be toggled to `true`
// from the app unless the user does so from a UI control.
// However, the user may have opt-ed out of mako experiments already, so
// we should respect that setting here.
val enabled =
context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
context.settings().isExperimentationEnabled
}
// The name "fenix" here corresponds to the app_name defined for the family of apps // These values can be used in the JEXL expressions when targeting experiments.
// that encompasses all of the channels for the Fenix app. This is defined upstream in val customTargetingAttributes = JSONObject().apply {
// the telemetry system. For more context on where the app_name come from see: // By convention, we should use snake case.
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings put("is_first_run", isAppFirstRun)
// and
// https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
val appInfo = NimbusAppInfo(
appName = "fenix",
// Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
// passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
// and would mostly produce the value `Beta` and rarely would produce `beta`.
channel = BuildConfig.BUILD_TYPE,
customTargetingAttributes = mapOf(
"isFirstRun" to context.settings().isFirstNimbusRun.toString(),
),
)
Nimbus(context, appInfo, serverSettings, errorReporter).apply {
// We register our own internal observer for housekeeping the Nimbus SDK and
// generated code.
register(observer)
val isFirstNimbusRun = context.settings().isFirstNimbusRun // This camelCase attribute is a boolean value represented as a string.
// This is left for backwards compatibility.
put("isFirstRun", isAppFirstRun.toString())
}
// We always want `Nimbus.initialize` to happen ASAP and before any features (engine/UI) // The name "fenix" here corresponds to the app_name defined for the family of apps
// have been initialized. For that reason, we use runBlocking here to avoid // that encompasses all of the channels for the Fenix app. This is defined upstream in
// inconsistency in the experiments. // the telemetry system. For more context on where the app_name come from see:
// We can safely do this because Nimbus does most of it's work on background threads, // https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// except for loading the initial experiments from disk. For this reason, we have a // and
// `joinOrTimeout` to limit the blocking until TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS. // https://github.com/mozilla/probe-scraper/blob/master/repositories.yaml
runBlockingIncrement { val appInfo = NimbusAppInfo(
val job = initialize( appName = "fenix",
isFirstNimbusRun || url.isNullOrBlank(), // Note: Using BuildConfig.BUILD_TYPE is important here so that it matches the value
R.raw.initial_experiments, // passed into Glean. `Config.channel.toString()` turned out to be non-deterministic
) // and would mostly produce the value `Beta` and rarely would produce `beta`.
// We only read from disk when loading first-run experiments. This is the only time channel = BuildConfig.BUILD_TYPE.let { if (it == "debug") "developer" else it },
// that we should join and block. Otherwise, we don't want to wait. customTargetingAttributes = customTargetingAttributes,
if (isFirstNimbusRun) { )
context.settings().isFirstNimbusRun = false
job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS)
}
}
if (!enabled) { return NimbusBuilder(context).apply {
// This opts out of nimbus experiments. It involves writing to disk, so does its url = urlString
// work on the db thread. errorReporter = { message, e ->
globalUserParticipation = enabled Logger.error("Nimbus error: $message", e)
if (e !is NimbusException || e.isReportableError()) {
context.components.analytics.crashReporter.submitCaughtException(e)
} }
} }
} catch (e: Throwable) { initialExperiments = R.raw.initial_experiments
// Something went wrong. We'd like not to, but stability of the app is more important than timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS
// failing fast here. usePreviewCollection = context.settings().nimbusUsePreview
errorReporter("Failed to initialize Nimbus", e) isFirstRun = isAppFirstRun
NimbusDisabled(context) onCreateCallback = { nimbus ->
} FxNimbus.initialize { nimbus }
}
onApplyCallback = {
FxNimbus.invalidateCachedValues()
}
}.build(appInfo)
} }
/** /**

@ -4,76 +4,12 @@
package org.mozilla.fenix.ext package org.mozilla.fenix.ext
import android.net.InetAddresses
import android.os.Build
import android.text.Editable import android.text.Editable
import android.util.Patterns
import android.webkit.URLUtil
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.core.net.toUri
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.compose.inComposePreview
import java.net.IDN
import java.util.Locale
const val FILE_PREFIX = "file://"
const val MAX_VALID_PORT = 65_535
/**
* Shortens URLs to be more user friendly.
*
* The algorithm used to generate these strings is a combination of FF desktop 'top sites',
* feedback from the security team, and documentation regarding url elision. See
* StringTest.kt for details.
*
* This method is complex because URLs have a lot of edge cases. Be sure to thoroughly unit
* test any changes you make to it.
*/
// Unused Parameter: We may resume stripping eTLD, depending on conversations between security and UX
// Return count: This is a complex method, but it would not be more understandable if broken up
// ComplexCondition: Breaking out the complex condition would make this logic harder to follow
@Suppress("UNUSED_PARAMETER", "ReturnCount", "ComplexCondition")
fun String.toShortUrl(publicSuffixList: PublicSuffixList): String {
val inputString = this
val uri = inputString.toUri()
if (
inputString.isEmpty() ||
!URLUtil.isValidUrl(inputString) ||
inputString.startsWith(FILE_PREFIX) ||
uri.port !in -1..MAX_VALID_PORT
) {
return inputString
}
if (uri.host?.isIpv4OrIpv6() == true ||
// If inputString is just a hostname and not a FQDN, use the entire hostname.
uri.host?.contains(".") == false
) {
return uri.host ?: inputString
}
fun String.stripUserInfo(): String {
val userInfo = this.toUri().encodedUserInfo
return if (userInfo != null) {
val infoIndex = this.indexOf(userInfo)
this.removeRange(infoIndex..infoIndex + userInfo.length)
} else {
this
}
}
fun String.stripPrefixes(): String = this.toUri().hostWithoutCommonPrefixes ?: this
fun String.toUnicode() = IDN.toUnicode(this)
return inputString
.stripUserInfo()
.lowercase(Locale.getDefault())
.stripPrefixes()
.toUnicode()
}
/** /**
* Shortens URLs to be more user friendly, by applying [String.toShortUrl] * Shortens URLs to be more user friendly, by applying [String.toShortUrl]
@ -94,29 +30,6 @@ fun String.toShortUrl(): String {
} }
} }
// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129
@Suppress("DEPRECATION")
internal fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches()
// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292
// True IPv6 validation is difficult. This is slightly better than nothing
internal fun String.isIpv6(): Boolean {
return this.isNotEmpty() && this.contains(":")
}
/**
* Returns true if the string represents a valid Ipv4 or Ipv6 IP address.
* Note: does not validate a dual format Ipv6 ( "y:y:y:y:y:y:x.x.x.x" format).
*
*/
fun String.isIpv4OrIpv6(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
InetAddresses.isNumericAddress(this)
} else {
this.isIpv4() || this.isIpv6()
}
}
/** /**
* Trims a URL string of its scheme and common prefixes. * Trims a URL string of its scheme and common prefixes.
* *

@ -43,11 +43,11 @@ fun View.removeTouchDelegate() {
fun View.setNewAccessibilityParent(newParent: View) { fun View.setNewAccessibilityParent(newParent: View) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() { this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo( override fun onInitializeAccessibilityNodeInfo(
host: View?, host: View,
info: AccessibilityNodeInfo?, info: AccessibilityNodeInfo,
) { ) {
super.onInitializeAccessibilityNodeInfo(host, info) super.onInitializeAccessibilityNodeInfo(host, info)
info?.setParent(newParent) info.setParent(newParent)
} }
} }
} }
@ -64,11 +64,22 @@ fun View.updateAccessibilityCollectionItemInfo(
) { ) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() { this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo( override fun onInitializeAccessibilityNodeInfo(
host: View?, host: View,
info: AccessibilityNodeInfo?, info: AccessibilityNodeInfo,
) { ) {
super.onInitializeAccessibilityNodeInfo(host, info) super.onInitializeAccessibilityNodeInfo(host, info)
info?.collectionItemInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
info.collectionItemInfo =
AccessibilityNodeInfo.CollectionItemInfo(
rowIndex,
rowSpan,
columnIndex,
columnSpan,
false,
isSelected,
)
} else {
@Suppress("DEPRECATION")
AccessibilityNodeInfo.CollectionItemInfo.obtain( AccessibilityNodeInfo.CollectionItemInfo.obtain(
rowIndex, rowIndex,
rowSpan, rowSpan,
@ -77,6 +88,7 @@ fun View.updateAccessibilityCollectionItemInfo(
false, false,
isSelected, isSelected,
) )
}
} }
} }
} }
@ -90,15 +102,24 @@ fun View.updateAccessibilityCollectionInfo(
) { ) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() { this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo( override fun onInitializeAccessibilityNodeInfo(
host: View?, host: View,
info: AccessibilityNodeInfo?, info: AccessibilityNodeInfo,
) { ) {
super.onInitializeAccessibilityNodeInfo(host, info) super.onInitializeAccessibilityNodeInfo(host, info)
info?.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
rowCount, info.collectionInfo = AccessibilityNodeInfo.CollectionInfo(
columnCount, rowCount,
false, columnCount,
) false,
)
} else {
@Suppress("DEPRECATION")
info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
rowCount,
columnCount,
false,
)
}
} }
} }
} }

@ -5,7 +5,6 @@
package org.mozilla.fenix.gleanplumb package org.mozilla.fenix.gleanplumb
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction.MessagingAction import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
@ -15,9 +14,7 @@ import org.mozilla.fenix.components.appstate.AppAction.MessagingAction
class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature { class MessagingFeature(val appStore: AppStore) : LifecycleAwareFeature {
override fun start() { override fun start() {
if (FeatureFlags.messagingFeature) { appStore.dispatch(MessagingAction.Evaluate)
appStore.dispatch(MessagingAction.Evaluate)
}
} }
override fun stop() = Unit override fun stop() = Unit

@ -28,6 +28,8 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -50,12 +52,17 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.menu.view.MenuButton import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.menu.Orientation
import mozilla.components.concept.menu.candidate.DrawableMenuIcon
import mozilla.components.concept.menu.candidate.TextMenuCandidate
import mozilla.components.concept.storage.FrecencyThresholdOption import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
@ -74,6 +81,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeScreen import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.UnifiedSearch
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.addons.showSnackBar
@ -88,6 +96,7 @@ import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.containsQueryParameters import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -115,6 +124,7 @@ import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.runBlockingIncrement import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.ToolbarPopupWindow import org.mozilla.fenix.utils.ToolbarPopupWindow
@ -143,6 +153,13 @@ class HomeFragment : Fragment() {
ToolbarPosition.TOP -> null ToolbarPosition.TOP -> null
} }
private val searchSelectorMenu by lazy {
SearchSelectorMenu(
context = requireContext(),
interactor = sessionControlInteractor,
)
}
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
@ -331,6 +348,24 @@ class HomeFragment : Fragment() {
) )
} }
requireContext().settings().showUnifiedSearchFeature.let {
binding.searchSelector.isVisible = it
binding.searchEngineIcon.isGone = it
}
binding.searchSelector.apply {
setOnClickListener {
val orientation = if (context.settings().shouldUseBottomToolbar) {
Orientation.UP
} else {
Orientation.DOWN
}
UnifiedSearch.searchMenuTapped.record(NoExtras())
searchSelectorMenu.menuController.show(anchor = it, orientation = orientation, forceOrientation = true)
}
}
_sessionControlInteractor = SessionControlInteractor( _sessionControlInteractor = SessionControlInteractor(
controller = DefaultSessionControlController( controller = DefaultSessionControlController(
activity = activity, activity = activity,
@ -595,6 +630,14 @@ class HomeFragment : Fragment() {
} }
} }
consumeFlow(requireComponents.core.store) { flow ->
flow.map { state -> state.search }
.ifChanged()
.collect { search ->
updateSearchSelectorMenu(search.searchEngines)
}
}
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL! // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker( requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, MarkersFragmentLifecycleCallbacks.MARKER_NAME,
@ -603,20 +646,42 @@ class HomeFragment : Fragment() {
) )
} }
private fun updateSearchSelectorMenu(searchEngines: List<SearchEngine>) {
val searchEngineList = searchEngines
.map {
TextMenuCandidate(
text = it.name,
start = DrawableMenuIcon(
drawable = it.icon.toDrawable(resources),
),
) {
sessionControlInteractor.onMenuItemTapped(SearchSelectorMenu.Item.SearchEngine(it))
}
}
searchSelectorMenu.menuController.submitList(searchSelectorMenu.menuItems(searchEngineList))
}
private fun observeSearchEngineChanges() { private fun observeSearchEngineChanges() {
consumeFlow(store) { flow -> consumeFlow(store) { flow ->
flow.map { state -> state.search.selectedOrDefaultSearchEngine } flow.map { state -> state.search.selectedOrDefaultSearchEngine }
.ifChanged() .ifChanged()
.collect { searchEngine -> .collect { searchEngine ->
if (searchEngine != null) { val name = searchEngine?.name
val icon = searchEngine?.let {
// Changing dimensions doesn't not affect the icon size, not sure what the
// code is doing: https://github.com/mozilla-mobile/fenix/issues/27763
val iconSize = val iconSize =
requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size) requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val searchIcon = BitmapDrawable(requireContext().resources, searchEngine.icon).apply {
BitmapDrawable(requireContext().resources, searchEngine.icon) setBounds(0, 0, iconSize, iconSize)
searchIcon.setBounds(0, 0, iconSize, iconSize) }
binding.searchEngineIcon.setImageDrawable(searchIcon) }
if (requireContext().settings().showUnifiedSearchFeature) {
binding.searchSelector.setIcon(icon, name)
} else { } else {
binding.searchEngineIcon.setImageDrawable(null) binding.searchEngineIcon.setImageDrawable(icon)
} }
} }
} }
@ -836,6 +901,8 @@ class HomeFragment : Fragment() {
true, true,
) )
layout.findViewById<Button>(R.id.cfr_pos_button).apply { layout.findViewById<Button>(R.id.cfr_pos_button).apply {
this.increaseTapArea(CFR_TAP_INCREASE_DPS)
setOnClickListener { setOnClickListener {
PrivateShortcutCreateManager.createPrivateShortcut(context) PrivateShortcutCreateManager.createPrivateShortcut(context)
privateBrowsingRecommend.dismiss() privateBrowsingRecommend.dismiss()
@ -1018,6 +1085,8 @@ class HomeFragment : Fragment() {
private const val CFR_WIDTH_DIVIDER = 1.7 private const val CFR_WIDTH_DIVIDER = 1.7
private const val CFR_Y_OFFSET = -20 private const val CFR_Y_OFFSET = -20
private const val CFR_TAP_INCREASE_DPS = 6
// Sponsored top sites titles and search engine names used for filtering // Sponsored top sites titles and search engine names used for filtering
const val AMAZON_SPONSORED_TITLE = "Amazon" const val AMAZON_SPONSORED_TITLE = "Amazon"
const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com" const val AMAZON_SEARCH_ENGINE_NAME = "Amazon.com"

@ -46,7 +46,6 @@ import org.mozilla.fenix.R.string
import org.mozilla.fenix.compose.list.FaviconListItem import org.mozilla.fenix.compose.list.FaviconListItem
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* Rectangular shape with only right angles used to display a middle tab. * Rectangular shape with only right angles used to display a middle tab.
@ -195,7 +194,7 @@ private fun Modifier.clipTop() = this.then(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun TabInCollectionPreview() { private fun TabInCollectionPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Column { Column {
Box(modifier = Modifier.height(56.dp)) { Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground( DismissedTabBackground(

@ -6,12 +6,17 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker.Companion.isDefaultBrowserNotificationIntent
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker.Companion.isReEngagementNotificationIntent
/** /**
* When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption] * When the default browser notification is tapped we need to launch [openSetDefaultBrowserOption]
@ -24,12 +29,26 @@ class DefaultBrowserIntentProcessor(
) : HomeIntentProcessor { ) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (isDefaultBrowserNotificationIntent(intent)) { return when {
activity.openSetDefaultBrowserOption() isDefaultBrowserNotificationIntent(intent) -> {
Events.defaultBrowserNotifTapped.record(NoExtras()) Events.defaultBrowserNotifTapped.record(NoExtras())
true
} else { activity.openSetDefaultBrowserOption()
false true
}
isReEngagementNotificationIntent(intent) -> {
Events.reEngagementNotifTapped.record(NoExtras())
activity.browsingModeManager.mode = BrowsingMode.Private
activity.openToBrowserAndLoad(
ReEngagementNotificationWorker.NOTIFICATION_TARGET_URL,
newTab = true,
from = BrowserDirection.FromGlobal,
flags = EngineSession.LoadUrlFlags.external(),
)
true
}
else -> false
} }
} }
} }

@ -4,6 +4,7 @@
package org.mozilla.fenix.home.pocket package org.mozilla.fenix.home.pocket
import android.content.res.Configuration
import android.view.View import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -12,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@ -25,7 +25,6 @@ import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.home.HomeSectionHeader import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.WallpaperState import org.mozilla.fenix.wallpapers.WallpaperState
internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8 internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8
@ -61,10 +60,8 @@ class PocketCategoriesViewHolder(
val wallpaperState = components.appStore val wallpaperState = components.appStore
.observeAsComposableState { state -> state.wallpaperState }.value ?: WallpaperState.default .observeAsComposableState { state -> state.wallpaperState }.value ?: WallpaperState.default
var selectedBackgroundColor: Color? = null var (selectedBackgroundColor, unselectedBackgroundColor, selectedTextColor, unselectedTextColor) =
var unselectedBackgroundColor: Color? = null PocketStoriesCategoryColors.buildColors()
var selectedTextColor: Color? = null
var unselectedTextColor: Color? = null
wallpaperState.composeRunIfWallpaperCardColorsAreAvailable { cardColorLight, cardColorDark -> wallpaperState.composeRunIfWallpaperCardColorsAreAvailable { cardColorLight, cardColorDark ->
if (isSystemInDarkTheme()) { if (isSystemInDarkTheme()) {
selectedBackgroundColor = cardColorDark selectedBackgroundColor = cardColorDark
@ -79,16 +76,20 @@ class PocketCategoriesViewHolder(
} }
} }
val categoryColors = PocketStoriesCategoryColors(
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
)
// See the detailed comment in PocketStoriesViewHolder for reasoning behind this change. // See the detailed comment in PocketStoriesViewHolder for reasoning behind this change.
if (!homeScreenReady) return if (!homeScreenReady) return
Column { Column {
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
PocketTopics( PocketTopics(
selectedBackgroundColor = selectedBackgroundColor, categoryColors = categoryColors,
unselectedBackgroundColor = unselectedBackgroundColor,
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
categories = categories ?: emptyList(), categories = categories ?: emptyList(),
categoriesSelections = categoriesSelections ?: emptyList(), categoriesSelections = categoriesSelections ?: emptyList(),
onCategoryClick = interactor::onCategoryClicked, onCategoryClick = interactor::onCategoryClicked,
@ -103,12 +104,9 @@ class PocketCategoriesViewHolder(
@Composable @Composable
private fun PocketTopics( private fun PocketTopics(
selectedTextColor: Color? = null,
unselectedTextColor: Color? = null,
selectedBackgroundColor: Color? = null,
unselectedBackgroundColor: Color? = null,
categories: List<PocketRecommendedStoriesCategory> = emptyList(), categories: List<PocketRecommendedStoriesCategory> = emptyList(),
categoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(), categoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(),
categoryColors: PocketStoriesCategoryColors = PocketStoriesCategoryColors.buildColors(),
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit, onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
) { ) {
Column { Column {
@ -121,21 +119,18 @@ private fun PocketTopics(
PocketStoriesCategories( PocketStoriesCategories(
categories = categories, categories = categories,
selections = categoriesSelections, selections = categoriesSelections,
selectedTextColor = selectedTextColor, modifier = Modifier.fillMaxWidth(),
unselectedTextColor = unselectedTextColor, categoryColors = categoryColors,
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
onCategoryClick = onCategoryClick, onCategoryClick = onCategoryClick,
modifier = Modifier
.fillMaxWidth(),
) )
} }
} }
@Composable @Composable
@Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PocketCategoriesViewHolderPreview() { private fun PocketCategoriesViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
PocketTopics( PocketTopics(
categories = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor" categories = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
.split(" ") .split(" ")

@ -27,7 +27,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.components import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* [RecyclerView.ViewHolder] for displaying the Pocket feature header. * [RecyclerView.ViewHolder] for displaying the Pocket feature header.
@ -83,7 +82,7 @@ class PocketRecommendationsHeaderViewHolder(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PocketRecommendationsFooterViewHolderPreview() { private fun PocketRecommendationsFooterViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) { Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
PoweredByPocketHeader( PoweredByPocketHeader(
onLearnMoreClicked = {}, onLearnMoreClicked = {},

@ -73,7 +73,6 @@ import org.mozilla.fenix.compose.TabSubtitleWithInterdot
import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -279,32 +278,43 @@ fun PocketStories(
itemsIndexed(storiesToShow) { columnIndex, columnItems -> itemsIndexed(storiesToShow) { columnIndex, columnItems ->
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
columnItems.forEachIndexed { rowIndex, story -> columnItems.forEachIndexed { rowIndex, story ->
if (story == placeholderStory) { Box(
ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) { modifier = Modifier.semantics {
onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE") testTagsAsResourceId = true
} testTag = when (story) {
} else if (story is PocketRecommendedStory) { placeholderStory -> "pocket.discover.more.story"
PocketStory( is PocketRecommendedStory -> "pocket.recommended.story"
story = story, else -> "pocket.sponsored.story"
backgroundColor = backgroundColor, }
) { },
val uri = Uri.parse(story.url) ) {
.buildUpon() if (story == placeholderStory) {
.appendQueryParameter(URI_PARAM_UTM_KEY, POCKET_STORIES_UTM_VALUE) ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) {
.build().toString() onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE")
onStoryClicked(it.copy(url = uri), rowIndex to columnIndex) }
} } else if (story is PocketRecommendedStory) {
} else if (story is PocketSponsoredStory) { PocketStory(
Box(
modifier = Modifier.onShown(0.5f) {
onStoryShown(story, rowIndex to columnIndex)
},
) {
PocketSponsoredStory(
story = story, story = story,
backgroundColor = backgroundColor, backgroundColor = backgroundColor,
) { ) {
onStoryClicked(story, rowIndex to columnIndex) val uri = Uri.parse(story.url)
.buildUpon()
.appendQueryParameter(URI_PARAM_UTM_KEY, POCKET_STORIES_UTM_VALUE)
.build().toString()
onStoryClicked(it.copy(url = uri), rowIndex to columnIndex)
}
} else if (story is PocketSponsoredStory) {
Box(
modifier = Modifier.onShown(0.5f) {
onStoryShown(story, rowIndex to columnIndex)
},
) {
PocketSponsoredStory(
story = story,
backgroundColor = backgroundColor,
) {
onStoryClicked(story, rowIndex to columnIndex)
}
} }
} }
} }
@ -408,12 +418,9 @@ private fun Rect.getIntersectPercentage(realSize: IntSize, other: Rect): Float {
* *
* @param categories The categories needed to be displayed. * @param categories The categories needed to be displayed.
* @param selections List of categories currently selected. * @param selections List of categories currently selected.
* @param selectedTextColor Text [Color] when the category is selected.
* @param unselectedTextColor Text [Color] when the category is not selected.
* @param selectedBackgroundColor Background [Color] when the category is selected.
* @param unselectedBackgroundColor Background [Color] when the category is not selected.
* @param onCategoryClick Callback for when the user taps a category.
* @param modifier [Modifier] to be applied to the layout. * @param modifier [Modifier] to be applied to the layout.
* @param categoryColors The color set defined by [PocketStoriesCategoryColors] used to style Pocket categories.
* @param onCategoryClick Callback for when the user taps a category.
*/ */
@OptIn(ExperimentalComposeUiApi::class) @OptIn(ExperimentalComposeUiApi::class)
@Suppress("LongParameterList") @Suppress("LongParameterList")
@ -421,12 +428,9 @@ private fun Rect.getIntersectPercentage(realSize: IntSize, other: Rect): Float {
fun PocketStoriesCategories( fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>, categories: List<PocketRecommendedStoriesCategory>,
selections: List<PocketRecommendedStoriesSelectedCategory>, selections: List<PocketRecommendedStoriesSelectedCategory>,
selectedTextColor: Color? = null,
unselectedTextColor: Color? = null,
selectedBackgroundColor: Color? = null,
unselectedBackgroundColor: Color? = null,
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
categoryColors: PocketStoriesCategoryColors = PocketStoriesCategoryColors.buildColors(),
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
) { ) {
Box( Box(
modifier = modifier.semantics { modifier = modifier.semantics {
@ -442,10 +446,10 @@ fun PocketStoriesCategories(
SelectableChip( SelectableChip(
text = category.name, text = category.name,
isSelected = selections.map { it.name }.contains(category.name), isSelected = selections.map { it.name }.contains(category.name),
selectedTextColor = selectedTextColor, selectedTextColor = categoryColors.selectedTextColor,
unselectedTextColor = unselectedTextColor, unselectedTextColor = categoryColors.unselectedTextColor,
selectedBackgroundColor = selectedBackgroundColor, selectedBackgroundColor = categoryColors.selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor, unselectedBackgroundColor = categoryColors.unselectedBackgroundColor,
) { ) {
onCategoryClick(category) onCategoryClick(category)
} }
@ -454,6 +458,40 @@ fun PocketStoriesCategories(
} }
} }
/**
* Wrapper for the color parameters of [PocketStoriesCategories].
*
* @param selectedTextColor Text [Color] when the category is selected.
* @param unselectedTextColor Text [Color] when the category is not selected.
* @param selectedBackgroundColor Background [Color] when the category is selected.
* @param unselectedBackgroundColor Background [Color] when the category is not selected.
*/
data class PocketStoriesCategoryColors(
val selectedBackgroundColor: Color,
val unselectedBackgroundColor: Color,
val selectedTextColor: Color,
val unselectedTextColor: Color,
) {
companion object {
/**
* Builder function used to construct an instance of [PocketStoriesCategoryColors].
*/
@Composable
fun buildColors(
selectedBackgroundColor: Color = FirefoxTheme.colors.actionPrimary,
unselectedBackgroundColor: Color = FirefoxTheme.colors.actionTertiary,
selectedTextColor: Color = FirefoxTheme.colors.textActionPrimary,
unselectedTextColor: Color = FirefoxTheme.colors.textActionTertiary,
) = PocketStoriesCategoryColors(
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
)
}
}
/** /**
* Pocket feature section title. * Pocket feature section title.
* Shows a default text about Pocket and offers a external link to learn more. * Shows a default text about Pocket and offers a external link to learn more.
@ -540,7 +578,7 @@ fun PoweredByPocketHeader(
@Composable @Composable
@Preview @Preview
private fun PocketStoriesComposablesPreview() { private fun PocketStoriesComposablesPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) { Box(Modifier.background(FirefoxTheme.colors.layer2)) {
Column { Column {
PocketStories( PocketStories(

@ -27,7 +27,6 @@ import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.home.HomeSectionHeader import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.WallpaperState import org.mozilla.fenix.wallpapers.WallpaperState
/** /**
@ -105,7 +104,7 @@ class PocketStoriesViewHolder(
@Composable @Composable
@Preview @Preview
fun PocketStoriesViewHolderPreview() { fun PocketStoriesViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
Column { Column {
HomeSectionHeader( HomeSectionHeader(
headerText = stringResource(R.string.pocket_stories_header_1), headerText = stringResource(R.string.pocket_stories_header_1),

@ -54,7 +54,6 @@ import org.mozilla.fenix.compose.Image
import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.home.recentbookmarks.RecentBookmark import org.mozilla.fenix.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
private val cardShape = RoundedCornerShape(8.dp) private val cardShape = RoundedCornerShape(8.dp)
@ -280,7 +279,7 @@ private fun RecentBookmarksMenu(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun RecentBookmarksPreview() { private fun RecentBookmarksPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
RecentBookmarks( RecentBookmarks(
bookmarks = listOf( bookmarks = listOf(
RecentBookmark( RecentBookmark(

@ -52,7 +52,6 @@ import org.mozilla.fenix.compose.button.SecondaryButton
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recenttabs.RecentTab import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* A recent synced tab card. * A recent synced tab card.
@ -287,7 +286,7 @@ private fun LoadedRecentSyncedTab() {
url = "https://mozilla.org", url = "https://mozilla.org",
previewImageUrl = "https://mozilla.org", previewImageUrl = "https://mozilla.org",
) )
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
RecentSyncedTab( RecentSyncedTab(
tab = tab, tab = tab,
onRecentSyncedTabClick = {}, onRecentSyncedTabClick = {},
@ -301,7 +300,7 @@ private fun LoadedRecentSyncedTab() {
@Preview @Preview
@Composable @Composable
private fun LoadingRecentSyncedTab() { private fun LoadingRecentSyncedTab() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
RecentSyncedTab( RecentSyncedTab(
tab = null, tab = null,
buttonBackgroundColor = FirefoxTheme.colors.layer3, buttonBackgroundColor = FirefoxTheme.colors.layer3,

@ -59,7 +59,6 @@ import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGroup
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
// Number of recently visited items per column. // Number of recently visited items per column.
private const val VISITS_PER_COLUMN = 3 private const val VISITS_PER_COLUMN = 3
@ -416,7 +415,7 @@ private val LazyListState.atLeastHalfVisibleItems
@Composable @Composable
@Preview @Preview
private fun RecentlyVisitedPreview() { private fun RecentlyVisitedPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
RecentlyVisited( RecentlyVisited(
recentVisits = listOf( recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"), RecentHistoryGroup(title = "running shoes"),

@ -40,6 +40,7 @@ import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.collections.SaveCollectionStep
@ -57,6 +58,8 @@ import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.Mode import org.mozilla.fenix.home.Mode
import org.mozilla.fenix.onboarding.WallpaperOnboardingDialogFragment.Companion.THUMBNAILS_SELECTION_COUNT import org.mozilla.fenix.onboarding.WallpaperOnboardingDialogFragment.Companion.THUMBNAILS_SELECTION_COUNT
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS import org.mozilla.fenix.settings.SupportUtils.SumoTopic.PRIVATE_BROWSING_MYTHS
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -209,6 +212,11 @@ interface SessionControlController {
* @see [SessionControlInteractor.reportSessionMetrics] * @see [SessionControlInteractor.reportSessionMetrics]
*/ */
fun handleReportSessionMetrics(state: AppState) fun handleReportSessionMetrics(state: AppState)
/**
* @see [SearchSelectorInteractor.onMenuItemTapped]
*/
fun handleMenuItemTapped(item: SearchSelectorMenu.Item)
} }
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList") @Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
@ -660,4 +668,26 @@ class DefaultSessionControlController(
RecentBookmarks.recentBookmarksCount.set(state.recentBookmarks.size.toLong()) RecentBookmarks.recentBookmarksCount.set(state.recentBookmarks.size.toLong())
} }
override fun handleMenuItemTapped(item: SearchSelectorMenu.Item) {
when (item) {
SearchSelectorMenu.Item.SearchSettings -> {
navController.nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalSearchEngineFragment(),
)
}
is SearchSelectorMenu.Item.SearchEngine -> {
val directions = HomeFragmentDirections.actionGlobalSearchDialog(
sessionId = null,
searchEngine = item.searchEngine.id,
)
navController.nav(
R.id.homeFragment,
directions,
BrowserAnimator.getToolbarNavOptions(activity),
)
}
}
}
} }

@ -27,6 +27,8 @@ import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryGrou
import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight import org.mozilla.fenix.home.recentvisits.RecentlyVisitedItem.RecentHistoryHighlight
import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController import org.mozilla.fenix.home.recentvisits.controller.RecentVisitsController
import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor import org.mozilla.fenix.home.recentvisits.interactor.RecentVisitsInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.wallpapers.WallpaperState import org.mozilla.fenix.wallpapers.WallpaperState
/** /**
@ -270,7 +272,8 @@ class SessionControlInteractor(
RecentBookmarksInteractor, RecentBookmarksInteractor,
RecentVisitsInteractor, RecentVisitsInteractor,
CustomizeHomeIteractor, CustomizeHomeIteractor,
PocketStoriesInteractor { PocketStoriesInteractor,
SearchSelectorInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) { override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection) controller.handleCollectionAddTabTapped(collection)
@ -485,4 +488,8 @@ class SessionControlInteractor(
override fun onMessageClosedClicked(message: Message) { override fun onMessageClosedClicked(message: Message) {
controller.handleMessageClosed(message) controller.handleMessageClosed(message)
} }
override fun onMenuItemTapped(item: SearchSelectorMenu.Item) {
controller.handleMenuItemTapped(item)
}
} }

@ -29,7 +29,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ComposeViewHolder import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor import org.mozilla.fenix.home.sessioncontrol.TabSessionInteractor
import org.mozilla.fenix.theme.FirefoxTheme import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/** /**
* View holder for a private browsing description. * View holder for a private browsing description.
@ -116,7 +115,7 @@ fun PrivateBrowsingDescription(
@Composable @Composable
@Preview @Preview
private fun PrivateBrowsingDescriptionPreview() { private fun PrivateBrowsingDescriptionPreview() {
FirefoxTheme(theme = Theme.getTheme()) { FirefoxTheme {
PrivateBrowsingDescription( PrivateBrowsingDescription(
onLearnMoreClick = {}, onLearnMoreClick = {},
) )

@ -31,6 +31,7 @@ import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.databinding.TopSiteItemBinding import org.mozilla.fenix.databinding.TopSiteItemBinding
import org.mozilla.fenix.ext.bitmapForUrl import org.mozilla.fenix.ext.bitmapForUrl
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -67,9 +68,22 @@ class TopSiteItemViewHolder(
is TopSiteItemMenu.Item.RenameTopSite -> interactor.onRenameTopSiteClicked( is TopSiteItemMenu.Item.RenameTopSite -> interactor.onRenameTopSiteClicked(
topSite, topSite,
) )
is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked( is TopSiteItemMenu.Item.RemoveTopSite -> {
topSite, interactor.onRemoveTopSiteClicked(topSite)
) FenixSnackbar.make(
view = it,
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = false,
)
.setText(it.context.getString(R.string.snackbar_top_site_removed))
.setAction(it.context.getString(R.string.snackbar_deleted_undo)) {
it.context.components.useCases.topSitesUseCase.addPinnedSites(
topSite.title.toString(),
topSite.url,
)
}
.show()
}
is TopSiteItemMenu.Item.Settings -> interactor.onSettingsClicked() is TopSiteItemMenu.Item.Settings -> interactor.onSettingsClicked()
is TopSiteItemMenu.Item.SponsorPrivacy -> interactor.onSponsorPrivacyClicked() is TopSiteItemMenu.Item.SponsorPrivacy -> interactor.onSponsorPrivacyClicked()
} }

@ -39,6 +39,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -54,7 +55,6 @@ import org.mozilla.fenix.ext.minus
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setTextColor import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo

@ -70,6 +70,7 @@ class BookmarkSearchDialogFragment : AppCompatDialogFragment(), UserInteractionH
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) { return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
this@BookmarkSearchDialogFragment.onBackPressed() this@BookmarkSearchDialogFragment.onBackPressed()
} }

@ -36,6 +36,7 @@ import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.NavHostActivity
@ -48,7 +49,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.placeCursorAtEnd import org.mozilla.fenix.ext.placeCursorAtEnd
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.setToolbarColors import org.mozilla.fenix.ext.setToolbarColors
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel import org.mozilla.fenix.library.bookmarks.BookmarksSharedViewModel
import org.mozilla.fenix.library.bookmarks.friendlyRootTitle import org.mozilla.fenix.library.bookmarks.friendlyRootTitle

@ -36,6 +36,7 @@ import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -52,7 +53,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.setTextColor import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.GleanMetrics.History as GleanHistory import org.mozilla.fenix.GleanMetrics.History as GleanHistory

@ -70,6 +70,7 @@ class HistorySearchDialogFragment : AppCompatDialogFragment(), UserInteractionHa
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) { return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
this@HistorySearchDialogFragment.onBackPressed() this@HistorySearchDialogFragment.onBackPressed()
} }

@ -27,6 +27,7 @@ import kotlinx.coroutines.flow.map
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.addons.showSnackBar import org.mozilla.fenix.addons.showSnackBar
@ -39,7 +40,6 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.setTextColor import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.library.history.History import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetadataGroupController import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetadataGroupController

@ -5,12 +5,9 @@
package org.mozilla.fenix.onboarding package org.mozilla.fenix.onboarding
import android.app.Notification import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -36,9 +33,15 @@ class DefaultBrowserNotificationWorker(
) : Worker(context, workerParameters) { ) : Worker(context, workerParameters) {
override fun doWork(): Result { override fun doWork(): Result {
val channelId = ensureChannelExists() val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext) NotificationManagerCompat.from(applicationContext)
.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(channelId)) .notify(
NOTIFICATION_TAG,
DEFAULT_BROWSER_NOTIFICATION_ID,
buildNotification(channelId),
)
Events.defaultBrowserNotifShown.record(NoExtras()) Events.defaultBrowserNotifShown.record(NoExtras())
// default browser notification should only happen once // default browser notification should only happen once
@ -81,50 +84,12 @@ class DefaultBrowserNotificationWorker(
} }
} }
/**
* Make sure a notification channel for default browser notification exists.
*
* Returns the channel id to be used for notifications.
*/
private fun ensureChannelExists(): String {
var channelEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
applicationContext.getString(R.string.notification_marketing_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
notificationManager.createNotificationChannel(channel)
val existingChannel = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
channelEnabled =
existingChannel != null && existingChannel.importance != NotificationManager.IMPORTANCE_NONE
}
@Suppress("TooGenericExceptionCaught")
val notificationsEnabled = try {
NotificationManagerCompat.from(applicationContext).areNotificationsEnabled()
} catch (e: Exception) {
false
}
marketingNotificationAllowed.set(notificationsEnabled && channelEnabled)
return NOTIFICATION_CHANNEL_ID
}
companion object { companion object {
private const val NOTIFICATION_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.default.browser" private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.default.browser"
private const val INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent" private const val INTENT_DEFAULT_BROWSER_NOTIFICATION = "org.mozilla.fenix.default.browser.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag" private const val NOTIFICATION_TAG = "org.mozilla.fenix.default.browser.tag"
private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.default.browser.work" private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.default.browser.work"
private const val NOTIFICATION_DELAY = Settings.ONE_DAY_MS private const val NOTIFICATION_DELAY = Settings.THREE_DAYS_MS
fun isDefaultBrowserNotificationIntent(intent: Intent) = fun isDefaultBrowserNotificationIntent(intent: Intent) =
intent.extras?.containsKey(INTENT_DEFAULT_BROWSER_NOTIFICATION) ?: false intent.extras?.containsKey(INTENT_DEFAULT_BROWSER_NOTIFICATION) ?: false

@ -0,0 +1,60 @@
/* 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.onboarding
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import androidx.core.app.NotificationManagerCompat
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.R
// Channel ID was not updated when it was renamed to marketing. Thus, we'll have to continue
// to use this ID as the marketing channel ID
private const val MARKETING_CHANNEL_ID = "org.mozilla.fenix.default.browser.channel"
// For notification that uses the marketing notification channel, IDs should be unique.
const val DEFAULT_BROWSER_NOTIFICATION_ID = 1
const val RE_ENGAGEMENT_NOTIFICATION_ID = 2
/**
* Make sure the marketing notification channel exists.
*
* Returns the channel id to be used for notifications.
*/
fun ensureMarketingChannelExists(context: Context): String {
var channelEnabled = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
var channel =
notificationManager.getNotificationChannel(MARKETING_CHANNEL_ID)
if (channel == null) {
channel = NotificationChannel(
MARKETING_CHANNEL_ID,
context.getString(R.string.notification_marketing_channel_name),
NotificationManager.IMPORTANCE_DEFAULT,
)
notificationManager.createNotificationChannel(channel)
}
channelEnabled = channel.importance != NotificationManager.IMPORTANCE_NONE
}
@Suppress("TooGenericExceptionCaught")
val notificationsEnabled = try {
NotificationManagerCompat.from(context).areNotificationsEnabled()
} catch (e: Exception) {
false
}
marketingNotificationAllowed.set(notificationsEnabled && channelEnabled)
return MARKETING_CHANNEL_ID
}

@ -0,0 +1,148 @@
/* 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.onboarding
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.annotation.VisibleForTesting
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import mozilla.components.support.base.ids.SharedIdsHelper
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.utils.IntentUtils
import org.mozilla.fenix.utils.Settings
import java.util.concurrent.TimeUnit
/**
* Worker that builds and schedules the re-engagement notification
*/
class ReEngagementNotificationWorker(
context: Context,
workerParameters: WorkerParameters,
) : Worker(context, workerParameters) {
override fun doWork(): Result {
val settings = applicationContext.settings()
if (isActiveUser(settings) || !settings.shouldShowReEngagementNotification()) {
return Result.success()
}
// Recording the exposure event here to capture all users who met all criteria to receive
// the re-engagement notification
FxNimbus.features.reEngagementNotification.recordExposure()
if (!settings.reEngagementNotificationEnabled) {
return Result.success()
}
val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext)
.notify(
NOTIFICATION_TAG,
RE_ENGAGEMENT_NOTIFICATION_ID,
buildNotification(channelId),
)
// re-engagement notification should only be shown once
settings.reEngagementNotificationShown = true
Events.reEngagementNotifShown.record(NoExtras())
return Result.success()
}
private fun buildNotification(channelId: String): Notification {
val intent = Intent(applicationContext, HomeActivity::class.java)
intent.putExtra(INTENT_RE_ENGAGEMENT_NOTIFICATION, true)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
SharedIdsHelper.getNextIdForTag(applicationContext, NOTIFICATION_PENDING_INTENT_TAG),
intent,
IntentUtils.defaultIntentPendingFlags,
)
with(applicationContext) {
val appName = getString(R.string.app_name)
return NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_status_logo)
.setContentTitle(
applicationContext.getString(R.string.notification_re_engagement_title),
)
.setContentText(
applicationContext.getString(R.string.notification_re_engagement_text, appName),
)
.setBadgeIconType(NotificationCompat.BADGE_ICON_SMALL)
.setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setShowWhen(false)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
}
}
companion object {
const val NOTIFICATION_TARGET_URL = "https://www.mozilla.org/firefox/privacy/"
private const val NOTIFICATION_PENDING_INTENT_TAG = "org.mozilla.fenix.re-engagement"
private const val INTENT_RE_ENGAGEMENT_NOTIFICATION = "org.mozilla.fenix.re-engagement.intent"
private const val NOTIFICATION_TAG = "org.mozilla.fenix.re-engagement.tag"
private const val NOTIFICATION_WORK_NAME = "org.mozilla.fenix.re-engagement.work"
private const val NOTIFICATION_DELAY = Settings.TWO_DAYS_MS
// We are trying to reach the users that are inactive after the initial 24 hours
private const val INACTIVE_USER_THRESHOLD = NOTIFICATION_DELAY - Settings.ONE_DAY_MS
/**
* Check if the intent is from the re-engagement notification
*/
fun isReEngagementNotificationIntent(intent: Intent) =
intent.extras?.containsKey(INTENT_RE_ENGAGEMENT_NOTIFICATION) ?: false
/**
* Schedules the re-engagement notification if needed.
*/
fun setReEngagementNotificationIfNeeded(context: Context) {
val instanceWorkManager = WorkManager.getInstance(context)
if (!context.settings().shouldSetReEngagementNotification()) {
return
}
val notificationWork = OneTimeWorkRequest.Builder(ReEngagementNotificationWorker::class.java)
.setInitialDelay(NOTIFICATION_DELAY, TimeUnit.MILLISECONDS)
.build()
instanceWorkManager.beginUniqueWork(
NOTIFICATION_WORK_NAME,
ExistingWorkPolicy.KEEP,
notificationWork,
).enqueue()
}
@VisibleForTesting
internal fun isActiveUser(settings: Settings): Boolean {
if (System.currentTimeMillis() - settings.lastBrowseActivity > INACTIVE_USER_THRESHOLD) {
return false
}
return true
}
}
}

@ -32,6 +32,7 @@ import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.crashes.CrashListActivity import org.mozilla.fenix.crashes.CrashListActivity
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -54,7 +55,7 @@ interface SearchController {
fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine) fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine)
/** /**
* @see [ToolbarInteractor.onMenuItemTapped] * @see [SearchSelectorInteractor.onMenuItemTapped]
*/ */
fun handleMenuItemTapped(item: SearchSelectorMenu.Item) fun handleMenuItemTapped(item: SearchSelectorMenu.Item)
} }

@ -154,6 +154,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) { return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
this@SearchDialogFragment.onBackPressed() this@SearchDialogFragment.onBackPressed()
} }
@ -178,6 +179,9 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
tabId = args.sessionId, tabId = args.sessionId,
pastedText = args.pastedText, pastedText = args.pastedText,
searchAccessPoint = args.searchAccessPoint, searchAccessPoint = args.searchAccessPoint,
searchEngine = requireComponents.core.store.state.search.searchEngines.firstOrNull {
it.id == args.searchEngine
},
), ),
) )
@ -385,6 +389,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
from = BrowserDirection.FromSearchDialog, from = BrowserDirection.FromSearchDialog,
) )
} }
requireContext().components.clipboardHandler.text = null
} }
val stubListener = ViewStub.OnInflateListener { _, inflated -> val stubListener = ViewStub.OnInflateListener { _, inflated ->
@ -447,7 +452,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
updateQrButton(it) updateQrButton(it)
} }
updateVoiceSearchButton(it) updateVoiceSearchButton()
} }
} }
@ -518,6 +523,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
qrFeature.get()?.let {
if (it.isScanInProgress) {
it.scan(binding.searchWrapper.id)
}
}
view?.post { view?.post {
// We delay querying the clipboard by posting this code to the main thread message queue, // We delay querying the clipboard by posting this code to the main thread message queue,
// because ClipboardManager will return null if the does app not have input focus yet. // because ClipboardManager will return null if the does app not have input focus yet.
@ -754,15 +765,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
searchSelectorAlreadyAdded = true searchSelectorAlreadyAdded = true
} }
private fun updateVoiceSearchButton(searchFragmentState: SearchFragmentState) { private fun updateVoiceSearchButton() {
val searchEngine = searchFragmentState.searchEngineSource.searchEngine when (isSpeechAvailable() && requireContext().settings().shouldShowVoiceSearch) {
val isVisible =
searchEngine?.id?.contains("google") == true &&
isSpeechAvailable() &&
requireContext().settings().shouldShowVoiceSearch
when (isVisible) {
true -> { true -> {
if (voiceSearchButtonAction == null) { if (voiceSearchButtonAction == null) {
voiceSearchButtonAction = IncreasedTapAreaActionDecorator( voiceSearchButtonAction = IncreasedTapAreaActionDecorator(

@ -117,12 +117,14 @@ data class SearchFragmentState(
/** /**
* Creates the initial state for the search fragment. * Creates the initial state for the search fragment.
*/ */
@Suppress("LongParameterList")
fun createInitialSearchFragmentState( fun createInitialSearchFragmentState(
activity: HomeActivity, activity: HomeActivity,
components: Components, components: Components,
tabId: String?, tabId: String?,
pastedText: String?, pastedText: String?,
searchAccessPoint: MetricsUtils.Source, searchAccessPoint: MetricsUtils.Source,
searchEngine: SearchEngine? = null,
): SearchFragmentState { ): SearchFragmentState {
val settings = components.settings val settings = components.settings
val tab = tabId?.let { components.core.store.state.findTab(it) } val tab = tabId?.let { components.core.store.state.findTab(it) }
@ -134,11 +136,17 @@ fun createInitialSearchFragmentState(
settings.shouldShowSearchSuggestions && settings.shouldShowSearchSuggestionsInPrivate settings.shouldShowSearchSuggestions && settings.shouldShowSearchSuggestionsInPrivate
} }
val searchEngineSource = if (searchEngine != null) {
SearchEngineSource.Shortcut(searchEngine)
} else {
SearchEngineSource.None
}
return SearchFragmentState( return SearchFragmentState(
query = url, query = url,
url = url, url = url,
searchTerms = tab?.content?.searchTerms.orEmpty(), searchTerms = tab?.content?.searchTerms.orEmpty(),
searchEngineSource = SearchEngineSource.None, searchEngineSource = searchEngineSource,
defaultEngine = null, defaultEngine = null,
showSearchSuggestions = shouldShowSearchSuggestions, showSearchSuggestions = shouldShowSearchSuggestions,
showSearchSuggestionsHint = false, showSearchSuggestionsHint = false,

@ -8,6 +8,7 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import org.mozilla.fenix.databinding.SearchSelectorBinding import org.mozilla.fenix.databinding.SearchSelectorBinding
@ -21,9 +22,21 @@ internal class SearchSelector @JvmOverloads constructor(
) : RelativeLayout(context, attrs, defStyle) { ) : RelativeLayout(context, attrs, defStyle) {
private val binding = SearchSelectorBinding.inflate(LayoutInflater.from(context), this) private val binding = SearchSelectorBinding.inflate(LayoutInflater.from(context), this)
private var marginTop: Int = 0
fun setIcon(icon: Drawable, contentDescription: String) { override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
if (params is MarginLayoutParams) {
params.topMargin = marginTop
}
super.setLayoutParams(params)
}
fun setIcon(icon: Drawable?, contentDescription: String?) {
binding.icon.setImageDrawable(icon) binding.icon.setImageDrawable(icon)
binding.icon.contentDescription = contentDescription binding.icon.contentDescription = contentDescription
} }
fun setTopMargin(margin: Int) {
marginTop = margin
}
} }

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

Loading…
Cancel
Save