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:
description: A feature measuring campaign growth data
hasExposure: true
@ -63,6 +71,14 @@ nimbus-validation:
settings-title:
type: string
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:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true

@ -122,7 +122,7 @@ tasks:
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})'
provisionerId: "${trustDomain}-${level}"
workerType: "decision"
workerType: "decision-gcp"
tags:
$if: 'tasks_for in ["github-push", "github-pull-request"]'
then:

@ -687,6 +687,7 @@ dependencies {
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation Deps.mozilla_support_rusthttp
androidTestImplementation Deps.mockk_android
testImplementation Deps.mockk
// 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
data_reviews:
- 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:
- technical
notification_emails:
@ -1105,6 +1131,7 @@ metrics:
to identify installs from Mozilla Online.
send_in_pings:
- metrics
- baseline
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16075
data_reviews:
@ -1116,6 +1143,8 @@ metrics:
- android-probes@mozilla.com
- kbrosnan@mozilla.com
expires: never
no_lint:
- BASELINE_PING
metadata:
tags:
- China
@ -1906,12 +1935,14 @@ customize_home:
An indication of whether Contile is enabled to be displayed
send_in_pings:
- metrics
- topsites-impression
bugs:
- https://github.com/mozilla-mobile/fenix/issues/24467
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/24468
data_sensitivity:
- interaction
lifetime: application
notification_emails:
- android-probes@mozilla.com
expires: 112
@ -6200,18 +6231,28 @@ browser.search:
type: labeled_counter
description: |
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:
- metrics
- baseline
bugs:
- 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:
- 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/19924#issuecomment-861423789
- 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/28012#issuecomment-1330822281
data_sensitivity:
- interaction
notification_emails:
@ -6224,18 +6265,27 @@ browser.search:
type: labeled_counter
description: |
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:
- metrics
- baseline
bugs:
- https://github.com/mozilla-mobile/fenix/issues/6558
- https://github.com/mozilla-mobile/fenix/issues/28010
data_reviews:
- 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/19924#issuecomment-861423789
- 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/28012#issuecomment-1330822281
data_sensitivity:
- interaction
notification_emails:
@ -6795,6 +6845,93 @@ autoplay:
tags:
- 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:
prompt_shown:
type: event

@ -18,6 +18,7 @@ object Constants {
}
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 LISTS_MAXSWIPES: Int = 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
import android.content.Intent
import android.view.ViewConfiguration.getLongPressTimeout
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule
@ -161,6 +162,8 @@ class HomeActivityIntentTestRule internal constructor(
private val longTapUserPreference = getLongPressTimeout()
private lateinit var intent: Intent
/**
* 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() {
super.beforeActivityLaunched()
setLongTapTimeout(3000)

@ -6,6 +6,7 @@
package org.mozilla.fenix.helpers
import android.Manifest
import android.app.ActivityManager
import android.app.PendingIntent
import android.content.ActivityNotFoundException
@ -20,8 +21,12 @@ import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.Settings
import android.util.Log
import android.view.View
import androidx.annotation.RequiresApi
import androidx.browser.customtabs.CustomTabsIntent
import androidx.test.espresso.Espresso
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.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.permission.PermissionRequester
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
@ -52,12 +57,13 @@ import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.Matcher
import org.junit.Assert
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
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.waitingTimeShort
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.utils.IntentUtils
import org.mozilla.gecko.util.ThreadUtils
import java.io.File
import java.util.Locale
import java.util.regex.Pattern
@ -147,24 +154,21 @@ object TestHelper {
}
}
// Remove test file from Google Photos (AOSP) on Firebase
fun deleteDownloadFromStorage() {
val deleteButton = mDevice.findObject(UiSelector().resourceId("$GOOGLE_APPS_PHOTOS:id/trash"))
deleteButton.waitForExists(waitingTime)
deleteButton.click()
// Sometimes there's a secondary confirmation
@RequiresApi(Build.VERSION_CODES.R)
fun deleteDownloadedFileOnStorage(fileName: String) {
val storageManager: StorageManager? = appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
val storageVolumes = storageManager!!.storageVolumes
val storageVolume: StorageVolume = storageVolumes[0]
val file = File(storageVolume.directory!!.path + "/Download/" + fileName)
try {
val deleteConfirm = mDevice.findObject(UiSelector().text("Got it"))
deleteConfirm.waitForExists(waitingTime)
deleteConfirm.click()
} catch (e: UiObjectNotFoundException) {
// Do nothing
file.delete()
Log.d("TestLog", "File delete try 1")
assertFalse("The file was not deleted", file.exists())
} catch (e: AssertionError) {
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) {
@ -388,20 +392,31 @@ object TestHelper {
/**
* 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.
* 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) {
val defaultLocale = Locale.getDefault()
if (Config.channel.isDebug) {
/* Sets permission to change device language */
PermissionRequester().apply {
addPermissions(
Manifest.permission.CHANGE_CONFIGURATION,
)
requestPermissions()
}
try {
setSystemLocale(locale)
testBlock()
ThreadUtils.runOnUiThread { testRule.activity.recreate() }
} catch (e: Exception) {
e.printStackTrace()
} finally {
setSystemLocale(defaultLocale)
val defaultLocale = Locale.getDefault()
try {
setSystemLocale(locale)
testBlock()
ThreadUtils.runOnUiThread { testRule.activity.recreate() }
} catch (e: Exception) {
e.printStackTrace()
} finally {
setSystemLocale(defaultLocale)
}
}
}

@ -33,7 +33,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
*
* 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

@ -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
fun verifyContextShareLink() {
val pageLinks =

@ -5,20 +5,14 @@
package org.mozilla.fenix.ui
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.Before
import org.junit.Ignore
import org.junit.Rule
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.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.downloadRobot
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.
**/
class DownloadTest {
private lateinit var mDevice: UiDevice
/* 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 var downloadFile: String = ""
@ -42,25 +34,8 @@ class DownloadTest {
@get:Rule
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
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
// clear all existing notifications
notificationShade {
mDevice.openNotification()
@ -157,13 +132,13 @@ class DownloadTest {
/* Verifies downloads in the Downloads Menu:
- downloads appear in the list
- 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
@Test
fun manageDownloadsInDownloadsMenuTest() {
// a long filename to verify it's correctly displayed on the prompt and in the Downloads menu
downloadFile = "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg"
downloadFile =
"tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFt.svg"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
@ -179,14 +154,34 @@ class DownloadTest {
waitForDownloadsListToExist()
verifyDownloadedFileName(downloadFile)
verifyDownloadedFileIcon()
openDownloadedFile(downloadFile)
verifyPhotosAppOpens()
deleteDownloadFromStorage()
waitForDownloadsListToExist()
deleteDownloadedFileOnStorage(downloadFile)
}.exitDownloadsManagerToBrowser {
}.openThreeDotMenu {
}.openDownloadsManager {
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
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
@ -11,9 +12,11 @@ import androidx.test.uiautomator.Until
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.POCKET_RECOMMENDED_STORIES_UTM_PARAM
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -34,11 +37,13 @@ class HomeScreenTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private lateinit var firstPocketStoryPublisher: String
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
@get:Rule(order = 0)
val activityTestRule =
AndroidComposeTestRule(HomeActivityTestRule.withDefaultSettingsOverrides()) { it.activity }
@Rule
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@ -62,21 +67,26 @@ class HomeScreenTest {
homeScreen { }.dismissOnboarding()
homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
verifyTabButton()
verifyCollectionsHeader()
verifyHomeToolbar()
verifyHomeComponent()
// Verify Top Sites
verifyExistingTopSitesList()
verifyHomePrivateBrowsingButton()
verifyExistingTopSitesTabs("Wikipedia")
verifyExistingTopSitesTabs("Top Articles")
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()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeMenuButton()
verifyHomeWordmark()
verifyTabButton()
verifyPrivateSessionMessage()
verifyHomeToolbar()
verifyNavigationToolbar()
verifyHomeComponent()
}
@ -104,15 +114,47 @@ class HomeScreenTest {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeMenuButton()
verifyHomeWordmark()
verifyTabButton()
verifyPrivateSessionMessage()
verifyHomeToolbar()
verifyNavigationToolbar()
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
fun dismissOnboardingUsingSettingsTest() {
homeScreen {
@ -141,7 +183,7 @@ class HomeScreenTest {
@Test
fun dismissOnboardingUsingHelpTest() {
activityTestRule.applySettingsExceptions {
activityTestRule.activityRule.applySettingsExceptions {
it.isJumpBackInCFREnabled = 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
fun toolbarTapDoesntDismissOnboardingTest() {
homeScreen {
@ -171,7 +232,7 @@ class HomeScreenTest {
@Test
fun verifyPocketHomepageStoriesTest() {
activityTestRule.applySettingsExceptions {
activityTestRule.activityRule.applySettingsExceptions {
it.isRecentTabsFeatureEnabled = false
it.isRecentlyVisitedFeatureEnabled = false
}
@ -181,6 +242,11 @@ class HomeScreenTest {
homeScreen {
verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7)
verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8)
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
verifyStoriesByTopic(true)
}.openThreeDotMenu {
}.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
fun verifyCustomizeHomepageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -12,11 +12,14 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
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.navigationToolbar
import java.util.Locale
/**
* 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:
// caution when making changes to it, so they don't block the builds
@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
@Test
fun searchGroupShowsInRecentlyVisitedTest() {
@ -194,6 +195,7 @@ class SearchTest {
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun verifySearchGroupHistoryWithNoDuplicatesTest() {
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
@Test
fun noSearchGroupFromPrivateBrowsingTest() {
@ -313,6 +316,7 @@ class SearchTest {
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@SmokeTest
@Test
fun deleteItemsFromSearchGroupHistoryTest() {
@ -361,6 +365,7 @@ class SearchTest {
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun deleteSearchGroupFromHistoryTest() {
queryString = "test search"
@ -407,6 +412,7 @@ class SearchTest {
}
}
@Ignore("Test run timing out: https://github.com/mozilla-mobile/fenix/issues/27704")
@Test
fun reopenTabsFromSearchGroupTest() {
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
fun sharePageFromASearchGroupTest() {
val firstPageUrl = getGenericAsset(searchMockServer, 1).url

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui
import android.content.res.Configuration
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -71,17 +70,6 @@ class SettingsBasicsTest {
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 settingsGeneralItemsTests() {
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
fun changeAccessibiltySettings() {
// 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")
@Test
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.RetryTestRule
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.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
@ -180,19 +182,6 @@ class SettingsHomepageTest {
fun startOnLastTabTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openHomepageSubMenu {
clickStartOnHomepageButton()
}
restartApp(activityIntentTestRule)
homeScreen {
verifyHomeScreen()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.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
@Test
@Ignore("Intermittent test: https://github.com/mozilla-mobile/fenix/issues/26559")

@ -420,12 +420,11 @@ class SettingsSearchTest {
"Bing",
"Amazon.com",
"DuckDuckGo",
"eBay",
/* Disabled Arabic Wikipedia verification
until https://github.com/mozilla-mobile/fenix/issues/12236 gets fixed
"ويكيبيديا (ar)"
*/
"ويكيبيديا (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.generateRandomString
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
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.openEditURLView
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:
@ -112,41 +110,19 @@ class SmokeTest {
@Test
fun firstRunScreenTest() {
homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
verifyWelcomeHeader()
// Sign in to Firefox
verifyStartSyncHeader()
verifyAccountsSignInButton()
// Always-on privacy
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
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()
verifyHomeScreenAppBarItems()
verifyHomeScreenWelcomeItems()
verifyChooseYourThemeCard(
isDarkThemeChecked = false,
isLightThemeChecked = false,
isAutomaticThemeChecked = true,
)
verifyToolbarPlacementCard(isBottomChecked = true, isTopChecked = false)
verifySignInToSyncCard()
verifyPrivacyProtectionCard(isStandardChecked = true, isStrictChecked = false)
verifyPrivacyNoticeCard()
verifyStartBrowsingSection()
verifyNavigationToolbarItems("0")
}
}
@ -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
@Test
fun updateSavedLoginTest() {

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

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

@ -8,15 +8,23 @@ package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap
import android.widget.EditText
import androidx.compose.ui.test.assert
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.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.performClick
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.PositionAssertions.isCompletelyAbove
import androidx.test.espresso.assertion.PositionAssertions.isPartiallyBelow
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
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.ext.waitNotNull
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.
@ -71,19 +76,93 @@ class HomeScreenRobot {
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyNavigationToolbar() = assertAppItemsWithResourceId(navigationToolbar)
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen()
fun verifyHomePrivateBrowsingButton() = assertHomePrivateBrowsingButton()
fun verifyHomeMenu() = assertHomeMenu()
fun verifyHomeScreen() = assertAppItemsWithResourceId(homeScreen)
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 verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertHomeWordmark()
fun verifyHomeToolbar() = assertHomeToolbar()
fun verifyHomeWordmark() = assertAppItemsWithResourceId(homepageWordmark)
fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyNoTabsOpened() = assertNoTabsOpened()
fun verifyTabCounter(numberOfOpenTabs: String) =
assertAppItemsWithResourceIdAndText(tabCounter(numberOfOpenTabs))
fun verifyKeyboardVisible() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifyWallpaperImageApplied(isEnabled: Boolean) {
@ -105,33 +184,12 @@ class HomeScreenRobot {
}
// First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader()
fun verifyStartSyncHeader() = assertStartSyncHeader()
fun verifyAccountsSignInButton() = assertAccountsSignInButton()
fun verifyChooseThemeHeader() = assertChooseThemeHeader()
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()
fun verifyWelcomeHeader() = assertAppItemsContainingText(welcomeHeader)
fun verifyAccountsSignInButton() = assertAppItemsWithResourceId(signInButton)
fun verifyStartBrowsingButton() {
scrollToElementByText(getStringResource(R.string.onboarding_finish))
assertAppItemsWithResourceId(startBrowsingButton)
}
// Upgrading users onboarding dialog
fun verifyUpgradingUserOnboardingFirstScreen(testRule: ComposeTestRule) {
@ -182,6 +240,9 @@ class HomeScreenRobot {
fun verifyJumpBackInSectionIsDisplayed() = assertJumpBackInSectionIsDisplayed()
fun verifyJumpBackInSectionIsNotDisplayed() = assertJumpBackInSectionIsNotDisplayed()
fun verifyJumpBackInItemTitle(itemTitle: String) = assertJumpBackInItemTitle(itemTitle)
fun verifyJumpBackInItemWithUrl(itemUrl: String) = assertJumpBackInItemWithUrl(itemUrl)
fun verifyJumpBackInShowAllButton() = assertJumpBackInShowAllButton()
fun verifyRecentlyVisitedSectionIsDisplayed() = assertRecentlyVisitedSectionIsDisplayed()
fun verifyRecentlyVisitedSectionIsNotDisplayed() = assertRecentlyVisitedSectionIsNotDisplayed()
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) {
if (enabled) {
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) {
if (enabled) {
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 {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
@ -366,8 +509,8 @@ class HomeScreenRobot {
}
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
navigationToolbar().waitForExists(waitingTime)
navigationToolbar().click()
navigationToolbar.waitForExists(waitingTime)
navigationToolbar.click()
SearchRobot().interact()
return SearchRobot.Transition()
@ -378,7 +521,7 @@ class HomeScreenRobot {
}
fun clickStartBrowsingButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
startBrowsingButton().click()
startBrowsingButton.click()
SearchRobot().interact()
return SearchRobot.Transition()
@ -399,8 +542,7 @@ class HomeScreenRobot {
.waitForExists(
waitingTime,
)
privateBrowsingButton()
.perform(click())
privateBrowsingButton.click()
}
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
@ -411,8 +553,7 @@ class HomeScreenRobot {
waitingTime,
)
privateBrowsingButton()
.perform(click())
privateBrowsingButton.click()
}
AddToHomeScreenRobot().interact()
@ -426,7 +567,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar().click()
navigationToolbar.click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -582,6 +723,63 @@ class HomeScreenRobot {
SettingsSubMenuHomepageRobot().interact()
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"),
)
private fun navigationToolbar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
private fun assertNavigationToolbar() = assertTrue(navigationToolbar().waitForExists(waitingTime))
private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address")))
.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() =
onView(allOf(withId(R.id.tab_button), isDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -655,8 +829,6 @@ private fun assertHomeComponent() =
onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
.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 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 }
private fun verifySearchEngineIcon(searchEngineName: String) {
val ddgSearchEngine = getSearchEngine(searchEngineName)
val defaultSearchEngine = getSearchEngine(searchEngineName)
?: throw AssertionError("No search engine with name $searchEngineName")
verifySearchEngineIcon(ddgSearchEngine.icon, ddgSearchEngine.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)))
verifySearchEngineIcon(defaultSearchEngine.icon, defaultSearchEngine.name)
}
private fun assertPrivateSessionMessage() =
@ -901,6 +941,35 @@ private fun assertJumpBackInSectionIsDisplayed() = assertTrue(jumpBackInSection(
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 assertRecentlyVisitedSectionIsNotDisplayed() = assertFalse(recentlyVisitedSection().waitForExists(waitingTime))
@ -915,8 +984,6 @@ private fun assertPocketSectionIsDisplayed() = assertTrue(pocketSection().waitFo
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 tabsCounter() = onView(withId(R.id.tab_button))
@ -933,15 +1000,6 @@ private fun recentBookmarksSection() =
private fun pocketSection() =
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) =
mDevice.findObject(
By
@ -949,6 +1007,129 @@ private fun sponsoredShortcut(sponsoredShortcutTitle: String) =
.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 =
onView(
allOf(

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

@ -537,3 +537,5 @@ private val awesomeBar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
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()
}
fun openCustomizeSubMenu(interact: SettingsSubMenuThemeRobot.() -> Unit): SettingsSubMenuThemeRobot.Transition {
fun openCustomizeSubMenu(interact: SettingsSubMenuCustomizeRobot.() -> Unit): SettingsSubMenuCustomizeRobot.Transition {
fun customizeButton() = onView(withText("Customize"))
customizeButton().click()
SettingsSubMenuThemeRobot().interact()
return SettingsSubMenuThemeRobot.Transition()
SettingsSubMenuCustomizeRobot().interact()
return SettingsSubMenuCustomizeRobot.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.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.junit.Assert.assertTrue
@ -97,7 +98,7 @@ private fun assertAboutToolbar() =
private fun assertVersionNumber() {
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 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
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 ROMANIAN_LANGUAGE_HEADER = "Limbă"
const val ARABIC_LANGUAGE_HEADER = "اللغة"

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

@ -31,41 +31,6 @@ object FeatureFlags {
*/
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.
*/
@ -82,36 +47,16 @@ object FeatureFlags {
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.
*/
val unifiedSearchFeature = Config.channel.isNightlyOrDebug
/**
* Enables receiving from the messaging framework.
*/
const val messagingFeature = true
/**
* Enables compose on the tabs tray items.
*/
val composeTabsTray = Config.channel.isDebug
/**
* Enables the wallpaper onboarding.
*/
const val wallpaperOnboardingEnabled = true
/**
* 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.log.Log
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.runOnlyInMainProcess
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.utils.logElapsedTime
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.AndroidAutofill
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.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.onboarding.ensureMarketingChannelExists
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
@ -134,12 +132,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
return
}
// We need to always initialize Glean and do it early here.
initializeGlean()
// DO NOT ADD ANYTHING ABOVE HERE.
setupInMainProcessOnly()
downloadWallpapers()
// DO NOT ADD ANYTHING UNDER HERE.
// DO NOT MOVE ANYTHING BELOW THIS elapsedRealtimeNanos CALL.
val stop = SystemClock.elapsedRealtimeNanos()
@ -197,11 +192,22 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@CallSuper
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()
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.
val megazordSetup = setupMegazord()
val megazordSetup = finishSetupMegazord()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
@ -222,6 +228,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
restoreBrowserState()
restoreDownloads()
restoreMessaging()
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
@ -244,6 +251,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
downloadWallpapers()
}
@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()
// 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()
queueRestoreLocale()
queueStorageMaintenance()
queueNotificationPermissionRequest()
}
private fun startMetricsIfEnabled() {
@ -415,6 +435,15 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
.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!
*
@ -424,54 +453,43 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
* Documentation on what megazords are, and why they're needed:
* - https://github.com/mozilla/application-services/blob/master/docs/design/megazords.md
* - 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 setupMegazord(): Deferred<Unit> {
private fun beginSetupMegazord() {
// Note: Megazord.init() must be called as soon as possible ...
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) {
initializeRustErrors(components.analytics.crashReporter)
// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
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)
if (Config.channel.isDebug) {
RustHttpConfig.allowEmulatorLoopback()
}
}
}
RustHttpConfig.setClient(lazy { components.core.client })
private fun setupNimbusObserver(nimbus: Observable<NimbusInterface.Observer>) {
nimbus.register(
object : NimbusInterface.Observer {
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
onNimbusStartupAndUpdate()
}
},
)
// Now viaduct (the RustHttp client) is initialized we can ask Nimbus to fetch
// experiments recipes from the server.
components.analytics.experiments.fetchExperiments()
}
}
private fun onNimbusStartupAndUpdate() {
// When Nimbus has successfully started up, we can apply our engine settings experiment.
// 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) {
private fun restoreMessaging() {
if (settings().isExperimentationEnabled) {
components.appStore.dispatch(AppAction.MessagingAction.Restore)
}
reportHomeScreenSectionMetrics(settings)
}
override fun onTrimMemory(level: Int) {

@ -42,6 +42,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
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.selector.getNormalOrPrivateTabs
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.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.onboarding.ReEngagementNotificationWorker
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
@ -275,7 +277,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
val safeIntent = intent?.toSafeIntent()
safeIntent
?.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()
@ -377,7 +383,13 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.appStore.dispatch(AppAction.ResumedMetricsAction)
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() {
@ -618,7 +630,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return
}
}
super.onBackPressed()
super.getOnBackPressedDispatcher().onBackPressed()
}
@Suppress("DEPRECATION")

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

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

@ -33,7 +33,6 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
@ -1251,7 +1250,7 @@ abstract class BaseBrowserFragment :
viewLifecycleOwner.lifecycleScope.launch(Main) {
val sitePermissions: SitePermissions? = tab.content.url.getOrigin()?.let { origin ->
val storage = requireComponents.core.permissionStorage
storage.findSitePermissionsBy(origin)
storage.findSitePermissionsBy(origin, tab.content.private)
}
view?.let {
@ -1370,6 +1369,7 @@ abstract class BaseBrowserFragment :
.setText(getString(R.string.full_screen_notification))
.show()
activity?.enterToImmersiveMode()
(view as? SwipeGestureLayout)?.isSwipeEnabled = false
browserToolbarView.collapse()
browserToolbarView.view.isVisible = false
val browserEngine = binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
@ -1384,6 +1384,7 @@ abstract class BaseBrowserFragment :
MediaState.fullscreen.record(NoExtras())
} else {
activity?.exitImmersiveMode()
(view as? SwipeGestureLayout)?.isSwipeEnabled = true
(activity as? HomeActivity)?.let { activity ->
activity.themeManager.applyStatusBarTheme(activity)
}
@ -1398,6 +1399,11 @@ abstract class BaseBrowserFragment :
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
*/
@ -1470,7 +1476,9 @@ abstract class BaseBrowserFragment :
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_browserToolbarView?.dismissMenu()
_browserToolbarView?.let {
onUpdateToolbarForConfigurationChange(it)
}
}
// 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.appcompat.content.res.AppCompatResources
import androidx.lifecycle.Observer
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
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.state.SessionState
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.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -52,6 +58,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private var readerModeAvailable = false
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")
override fun initializeUI(view: View, tab: SessionState) {
super.initializeUI(view, tab)
@ -84,86 +95,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
browserToolbarView.view.addNavigationAction(homeAction)
if (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)
}
updateToolbarActions(isTablet = resources.getBoolean(R.bool.tablet))
val readerModeAction =
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() {
super.onStart()
val context = requireContext()
@ -261,6 +328,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
subscribeToTabCollections()
updateLastBrowseActivity()
}
override fun onStop() {
@ -297,22 +365,35 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached {
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
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,
val useCase = requireComponents.useCases.trackingProtectionUseCases
FxNimbus.features.cookieBanners.recordExposure()
useCase.containsException(tab.id) { hasTrackingProtectionException ->
lifecycleScope.launch(Dispatchers.Main) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
val hasCookieBannerException = withContext(Dispatchers.IO) {
cookieBannersStorage.hasException(
tab.content.url,
tab.content.private,
)
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,
) : FrameLayout(context, attrs, defStyleAttr) {
/**
* Controls whether the swiping functionality is active or not.
*/
var isSwipeEnabled = true
private val gestureListener = object : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean {
override fun onDown(e: MotionEvent): Boolean {
return true
}
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent?,
e1: MotionEvent,
e2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false
val next = e2?.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) }
if (activeListener == null && !handledInitialScroll) {
activeListener = listeners.firstOrNull { listener ->
@ -81,8 +86,8 @@ class SwipeGestureLayout @JvmOverloads constructor(
}
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent?,
e1: MotionEvent,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,
): Boolean {
@ -107,6 +112,10 @@ class SwipeGestureLayout @JvmOverloads constructor(
}
override fun onInterceptTouchEvent(event: MotionEvent): Boolean {
if (!isSwipeEnabled) {
return false
}
return when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
handledInitialScroll = false

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

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

@ -20,12 +20,12 @@ import androidx.transition.TransitionManager
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.ComponentCollectionCreationBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl
class CollectionCreationView(
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.lazyMonitored
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
@ -127,7 +126,7 @@ class BackgroundServices(
}
private val telemetryAccountObserver = TelemetryAccountObserver(
context.settings(),
context,
)
val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode)
@ -219,13 +218,16 @@ private class AccountManagerReadyObserver(
@VisibleForTesting(otherwise = PRIVATE)
internal class TelemetryAccountObserver(
private val settings: Settings,
private val context: Context,
) : AccountObserver {
override fun onAuthenticated(account: OAuthAccount, authType: AuthType) {
settings.signedInFxaAccount = true
context.settings().signedInFxaAccount = true
when (authType) {
// 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.
AuthType.Signup -> SyncAuth.signUp.record(NoExtras())
@ -254,6 +256,6 @@ internal class TelemetryAccountObserver(
override fun onLoggedOut() {
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.graphics.drawable.toBitmap
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.permission.GeckoSitePermissionsStorage
import mozilla.components.browser.icons.BrowserIcons
@ -132,6 +133,8 @@ class Core(
R.color.fx_mobile_layer_color_1,
),
httpsOnlyMode = context.settings().getHttpsOnlyMode(),
cookieBannerHandlingModePrivateBrowsing = context.settings().getCookieBannerHandling(),
cookieBannerHandlingMode = context.settings().getCookieBannerHandling(),
)
GeckoEngine(
@ -181,6 +184,8 @@ class Core(
)
}
val cookieBannersStorage by lazyMonitored { GeckoCookieBannersStorage(geckoRuntime) }
val geckoSitePermissionsStorage by lazyMonitored {
GeckoSitePermissionsStorage(geckoRuntime, OnDiskSitePermissionsStorage(context))
}

@ -20,26 +20,58 @@ class PermissionStorage(
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) {
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> {
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) {
permissionsStorage.remove(sitePermissions)
permissionsStorage.remove(sitePermissions, private = false)
}
/**
* Deletes all sitePermissions sitePermissions.
*/
suspend fun deleteAllSitePermissions() = withContext(dispatcher) {
permissionsStorage.removeAll()
}

@ -19,8 +19,8 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage
import mozilla.components.support.base.observer.Observable
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.toShortUrl
import org.mozilla.fenix.perf.StrictModeManager
private const val COLLECTION_MAX_TITLE_LENGTH = 20

@ -23,14 +23,9 @@ sealed class Event {
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")
/**
* Event recording the first time a URI is loaded in Firefox in a 24 hour period.
*/
object FirstUriLoadForDay : GrowthData("ja86ek")
object SerpAdClicked : GrowthData("e2x17e")
/**
* 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 -> {
BrowserSearch.adClicks[value!!].add()
track(Event.GrowthData.SerpAdClicked)
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> {
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
import mozilla.components.lib.state.Middleware
@ -23,7 +27,6 @@ class MetricsMiddleware(
private fun handleAction(action: AppAction) = when (action) {
is AppAction.ResumedMetricsAction -> {
metrics.track(Event.GrowthData.SetAsDefault)
metrics.track(Event.GrowthData.FirstAppOpenForDay)
metrics.track(Event.GrowthData.FirstWeekSeriesActivity)
}
else -> Unit

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

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

@ -22,7 +22,7 @@ import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior
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.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.customtabs.CustomTabToolbarIntegration

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

@ -17,7 +17,6 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
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.
@ -95,7 +94,7 @@ fun ClickableSubstringLink(
private fun ClickableSubstringTextPreview() {
val text = "This text contains a link"
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
ClickableSubstringLink(
text = text,

@ -24,7 +24,6 @@ import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Load and display the favicon of a particular website.
@ -98,7 +97,7 @@ private fun FaviconPlaceholder(
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun FaviconPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
Favicon(
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.sp
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.
@ -171,7 +170,7 @@ fun ListItemTabSurface(
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ListItemTabLargePreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
ListItemTabLarge(
imageUrl = "",
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_NO)
private fun ListItemTabSurfacePreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
ListItemTabSurface(
imageUrl = "",
) {
@ -201,7 +200,7 @@ private fun ListItemTabSurfacePreview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun ListItemTabSurfaceWithCustomBackgroundPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
ListItemTabSurface(
imageUrl = "",
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.sp
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.
@ -74,7 +73,7 @@ fun ListItemTabLargePlaceholder(
@Composable
@Preview
private fun ListItemTabLargePlaceholderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
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.sp
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Default layout of a selectable chip.
@ -78,7 +77,7 @@ fun SelectableChip(
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO)
private fun SelectableChipPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Row(
modifier = Modifier
.fillMaxWidth()
@ -95,7 +94,7 @@ private fun SelectableChipPreview() {
@Preview(uiMode = UI_MODE_NIGHT_YES)
@Preview(uiMode = UI_MODE_NIGHT_NO)
private fun SelectableChipWithCustomColorsPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Row(
modifier = Modifier
.fillMaxWidth()

@ -20,7 +20,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
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
@ -121,7 +120,7 @@ fun StaggeredHorizontalGrid(
@Composable
@Preview
private fun StaggeredHorizontalGridPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) {
StaggeredHorizontalGrid(
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.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Special caption text for a tab layout shown on one line.
@ -106,7 +105,7 @@ fun TabSubtitleWithInterdot(
@Composable
@Preview
private fun TabSubtitleWithInterdotPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) {
TabSubtitleWithInterdot(
firstText = "firstText",

@ -34,7 +34,6 @@ import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
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
@ -138,7 +137,7 @@ private fun ThumbnailImage(
@Preview
@Composable
private fun ThumbnailCardPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
ThumbnailCard(
url = "https://mozilla.com",
key = "123",

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

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

@ -28,7 +28,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.Wallpaper
/**
@ -124,7 +123,7 @@ private fun HomeSectionHeaderContent(
@Composable
@Preview
private fun HomeSectionsHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
HomeSectionHeader(
headerText = stringResource(R.string.recently_saved_title),
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 org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* Expandable header for sections of lists
@ -96,7 +95,7 @@ fun ExpandableListHeader(
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextOnlyHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(headerText = "Section title")
}
@ -106,7 +105,7 @@ private fun TextOnlyHeaderPreview() {
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun CollapsibleHeaderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(
headerText = "Collapsible section title",
@ -122,7 +121,7 @@ private fun CollapsibleHeaderPreview() {
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun HeaderWithClickableIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(headerText = "Section title") {
Box(
@ -145,7 +144,7 @@ private fun HeaderWithClickableIconPreview() {
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun CollapsibleHeaderWithClickableIconPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
ExpandableListHeader(
headerText = "Section title",

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

@ -34,7 +34,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* 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_NO)
private fun TabListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
TabListItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
onCloseClick = {},
@ -187,7 +186,7 @@ private fun TabListItemPreview() {
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun SelectedTabListItemPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
TabListItem(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
onCloseClick = {},

@ -9,7 +9,11 @@ import android.content.Intent
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
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.toolbar.BrowserToolbar
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.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.components
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
@ -159,21 +164,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val cookieBannersStorage = requireComponents.core.cookieBannersStorage
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment(
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 = tab.trackingProtection.enabled && !contains,
)
nav(R.id.externalAppBrowserFragment, directions)
lifecycleScope.launch(Dispatchers.IO) {
val hasException =
cookieBannersStorage.hasException(tab.content.url, tab.content.private)
withContext(Dispatchers.Main) {
runIfFragmentIsAttached {
val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment(
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 = tab.trackingProtection.enabled && !contains,
isCookieHandlingEnabled = !hasException,
)
nav(R.id.externalAppBrowserFragment, directions)
}
}
}
}
}

@ -5,36 +5,17 @@
package org.mozilla.fenix.experiments
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.NimbusAppInfo
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.service.nimbus.NimbusServerSettings
import mozilla.components.service.nimbus.NimbusBuilder
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.experiments.nimbus.NimbusInterface
import org.mozilla.experiments.nimbus.internal.EnrolledExperiment
import org.json.JSONObject
import org.mozilla.experiments.nimbus.internal.NimbusException
import org.mozilla.experiments.nimbus.joinOrTimeout
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
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.
@ -44,93 +25,59 @@ private val observer = object : NimbusInterface.Observer {
*/
private const val TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS = 200L
@Suppress("TooGenericExceptionCaught")
fun createNimbus(context: Context, url: String?): NimbusApi {
val errorReporter: ((String, Throwable) -> Unit) = reporter@{ message, e ->
Logger.error("Nimbus error: $message", e)
if (e is NimbusException && !e.isReportableError()) {
return@reporter
}
context.components.analytics.crashReporter.submitCaughtException(e)
/**
* Create the Nimbus singleton object for the Fenix app.
*/
fun createNimbus(context: Context, urlString: String?): NimbusApi {
val isAppFirstRun = context.settings().isFirstNimbusRun
if (isAppFirstRun) {
context.settings().isFirstNimbusRun = false
}
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
// that encompasses all of the channels for the Fenix app. This is defined upstream in
// the telemetry system. For more context on where the app_name come from see:
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// 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)
// These values can be used in the JEXL expressions when targeting experiments.
val customTargetingAttributes = JSONObject().apply {
// By convention, we should use snake case.
put("is_first_run", isAppFirstRun)
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)
// have been initialized. For that reason, we use runBlocking here to avoid
// inconsistency in the experiments.
// We can safely do this because Nimbus does most of it's work on background threads,
// except for loading the initial experiments from disk. For this reason, we have a
// `joinOrTimeout` to limit the blocking until TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS.
runBlockingIncrement {
val job = initialize(
isFirstNimbusRun || url.isNullOrBlank(),
R.raw.initial_experiments,
)
// We only read from disk when loading first-run experiments. This is the only time
// that we should join and block. Otherwise, we don't want to wait.
if (isFirstNimbusRun) {
context.settings().isFirstNimbusRun = false
job.joinOrTimeout(TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS)
}
}
// The name "fenix" here corresponds to the app_name defined for the family of apps
// that encompasses all of the channels for the Fenix app. This is defined upstream in
// the telemetry system. For more context on where the app_name come from see:
// https://probeinfo.telemetry.mozilla.org/v2/glean/app-listings
// 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.let { if (it == "debug") "developer" else it },
customTargetingAttributes = customTargetingAttributes,
)
if (!enabled) {
// This opts out of nimbus experiments. It involves writing to disk, so does its
// work on the db thread.
globalUserParticipation = enabled
return NimbusBuilder(context).apply {
url = urlString
errorReporter = { message, e ->
Logger.error("Nimbus error: $message", e)
if (e !is NimbusException || e.isReportableError()) {
context.components.analytics.crashReporter.submitCaughtException(e)
}
}
} catch (e: Throwable) {
// Something went wrong. We'd like not to, but stability of the app is more important than
// failing fast here.
errorReporter("Failed to initialize Nimbus", e)
NimbusDisabled(context)
}
initialExperiments = R.raw.initial_experiments
timeoutLoadingExperiment = TIME_OUT_LOADING_EXPERIMENT_FROM_DISK_MS
usePreviewCollection = context.settings().nimbusUsePreview
isFirstRun = isAppFirstRun
onCreateCallback = { nimbus ->
FxNimbus.initialize { nimbus }
}
onApplyCallback = {
FxNimbus.invalidateCachedValues()
}
}.build(appInfo)
}
/**

@ -4,76 +4,12 @@
package org.mozilla.fenix.ext
import android.net.InetAddresses
import android.os.Build
import android.text.Editable
import android.util.Patterns
import android.webkit.URLUtil
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.toShortUrl
import org.mozilla.fenix.components.components
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]
@ -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.
*

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

@ -5,7 +5,6 @@
package org.mozilla.fenix.gleanplumb
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.components.AppStore
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 {
override fun start() {
if (FeatureFlags.messagingFeature) {
appStore.dispatch(MessagingAction.Evaluate)
}
appStore.dispatch(MessagingAction.Evaluate)
}
override fun stop() = Unit

@ -28,6 +28,8 @@ import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.TOP
import androidx.coordinatorlayout.widget.CoordinatorLayout
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.updateLayoutParams
import androidx.fragment.app.Fragment
@ -50,12 +52,17 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
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.normalTabs
import mozilla.components.browser.state.selector.privateTabs
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.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.sync.AccountObserver
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.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.UnifiedSearch
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
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.containsQueryParameters
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
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.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.tabstray.TabsTrayAccessPoint
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.utils.ToolbarPopupWindow
@ -143,6 +153,13 @@ class HomeFragment : Fragment() {
ToolbarPosition.TOP -> null
}
private val searchSelectorMenu by lazy {
SearchSelectorMenu(
context = requireContext(),
interactor = sessionControlInteractor,
)
}
private val browsingModeManager get() = (activity as HomeActivity).browsingModeManager
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(
controller = DefaultSessionControlController(
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!
requireComponents.core.engine.profiler?.addMarker(
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() {
consumeFlow(store) { flow ->
flow.map { state -> state.search.selectedOrDefaultSearchEngine }
.ifChanged()
.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 =
requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val searchIcon =
BitmapDrawable(requireContext().resources, searchEngine.icon)
searchIcon.setBounds(0, 0, iconSize, iconSize)
binding.searchEngineIcon.setImageDrawable(searchIcon)
BitmapDrawable(requireContext().resources, searchEngine.icon).apply {
setBounds(0, 0, iconSize, iconSize)
}
}
if (requireContext().settings().showUnifiedSearchFeature) {
binding.searchSelector.setIcon(icon, name)
} else {
binding.searchEngineIcon.setImageDrawable(null)
binding.searchEngineIcon.setImageDrawable(icon)
}
}
}
@ -836,6 +901,8 @@ class HomeFragment : Fragment() {
true,
)
layout.findViewById<Button>(R.id.cfr_pos_button).apply {
this.increaseTapArea(CFR_TAP_INCREASE_DPS)
setOnClickListener {
PrivateShortcutCreateManager.createPrivateShortcut(context)
privateBrowsingRecommend.dismiss()
@ -1018,6 +1085,8 @@ class HomeFragment : Fragment() {
private const val CFR_WIDTH_DIVIDER = 1.7
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
const val AMAZON_SPONSORED_TITLE = "Amazon"
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.ext.toShortUrl
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* 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_NO)
private fun TabInCollectionPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Column {
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(

@ -6,12 +6,17 @@ package org.mozilla.fenix.home.intent
import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.ext.settings
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]
@ -24,12 +29,26 @@ class DefaultBrowserIntentProcessor(
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (isDefaultBrowserNotificationIntent(intent)) {
activity.openSetDefaultBrowserOption()
Events.defaultBrowserNotifTapped.record(NoExtras())
true
} else {
false
return when {
isDefaultBrowserNotificationIntent(intent) -> {
Events.defaultBrowserNotifTapped.record(NoExtras())
activity.openSetDefaultBrowserOption()
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
import android.content.res.Configuration
import android.view.View
import androidx.compose.foundation.isSystemInDarkTheme
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.stringResource
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.home.HomeSectionHeader
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.WallpaperState
internal const val POCKET_CATEGORIES_SELECTED_AT_A_TIME_COUNT = 8
@ -61,10 +60,8 @@ class PocketCategoriesViewHolder(
val wallpaperState = components.appStore
.observeAsComposableState { state -> state.wallpaperState }.value ?: WallpaperState.default
var selectedBackgroundColor: Color? = null
var unselectedBackgroundColor: Color? = null
var selectedTextColor: Color? = null
var unselectedTextColor: Color? = null
var (selectedBackgroundColor, unselectedBackgroundColor, selectedTextColor, unselectedTextColor) =
PocketStoriesCategoryColors.buildColors()
wallpaperState.composeRunIfWallpaperCardColorsAreAvailable { cardColorLight, cardColorDark ->
if (isSystemInDarkTheme()) {
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.
if (!homeScreenReady) return
Column {
Spacer(Modifier.height(24.dp))
PocketTopics(
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
categoryColors = categoryColors,
categories = categories ?: emptyList(),
categoriesSelections = categoriesSelections ?: emptyList(),
onCategoryClick = interactor::onCategoryClicked,
@ -103,12 +104,9 @@ class PocketCategoriesViewHolder(
@Composable
private fun PocketTopics(
selectedTextColor: Color? = null,
unselectedTextColor: Color? = null,
selectedBackgroundColor: Color? = null,
unselectedBackgroundColor: Color? = null,
categories: List<PocketRecommendedStoriesCategory> = emptyList(),
categoriesSelections: List<PocketRecommendedStoriesSelectedCategory> = emptyList(),
categoryColors: PocketStoriesCategoryColors = PocketStoriesCategoryColors.buildColors(),
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
) {
Column {
@ -121,21 +119,18 @@ private fun PocketTopics(
PocketStoriesCategories(
categories = categories,
selections = categoriesSelections,
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
modifier = Modifier.fillMaxWidth(),
categoryColors = categoryColors,
onCategoryClick = onCategoryClick,
modifier = Modifier
.fillMaxWidth(),
)
}
}
@Composable
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PocketCategoriesViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
PocketTopics(
categories = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
.split(" ")

@ -27,7 +27,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* [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_NO)
private fun PocketRecommendationsFooterViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) {
PoweredByPocketHeader(
onLearnMoreClicked = {},

@ -73,7 +73,6 @@ import org.mozilla.fenix.compose.TabSubtitleWithInterdot
import org.mozilla.fenix.compose.inComposePreview
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@ -279,32 +278,43 @@ fun PocketStories(
itemsIndexed(storiesToShow) { columnIndex, columnItems ->
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
columnItems.forEachIndexed { rowIndex, story ->
if (story == placeholderStory) {
ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) {
onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE")
}
} else if (story is PocketRecommendedStory) {
PocketStory(
story = story,
backgroundColor = backgroundColor,
) {
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(
Box(
modifier = Modifier.semantics {
testTagsAsResourceId = true
testTag = when (story) {
placeholderStory -> "pocket.discover.more.story"
is PocketRecommendedStory -> "pocket.recommended.story"
else -> "pocket.sponsored.story"
}
},
) {
if (story == placeholderStory) {
ListItemTabLargePlaceholder(stringResource(R.string.pocket_stories_placeholder_text)) {
onDiscoverMoreClicked("https://getpocket.com/explore?$POCKET_FEATURE_UTM_KEY_VALUE")
}
} else if (story is PocketRecommendedStory) {
PocketStory(
story = story,
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 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 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)
@Suppress("LongParameterList")
@ -421,12 +428,9 @@ private fun Rect.getIntersectPercentage(realSize: IntSize, other: Rect): Float {
fun PocketStoriesCategories(
categories: List<PocketRecommendedStoriesCategory>,
selections: List<PocketRecommendedStoriesSelectedCategory>,
selectedTextColor: Color? = null,
unselectedTextColor: Color? = null,
selectedBackgroundColor: Color? = null,
unselectedBackgroundColor: Color? = null,
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
modifier: Modifier = Modifier,
categoryColors: PocketStoriesCategoryColors = PocketStoriesCategoryColors.buildColors(),
onCategoryClick: (PocketRecommendedStoriesCategory) -> Unit,
) {
Box(
modifier = modifier.semantics {
@ -442,10 +446,10 @@ fun PocketStoriesCategories(
SelectableChip(
text = category.name,
isSelected = selections.map { it.name }.contains(category.name),
selectedTextColor = selectedTextColor,
unselectedTextColor = unselectedTextColor,
selectedBackgroundColor = selectedBackgroundColor,
unselectedBackgroundColor = unselectedBackgroundColor,
selectedTextColor = categoryColors.selectedTextColor,
unselectedTextColor = categoryColors.unselectedTextColor,
selectedBackgroundColor = categoryColors.selectedBackgroundColor,
unselectedBackgroundColor = categoryColors.unselectedBackgroundColor,
) {
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.
* Shows a default text about Pocket and offers a external link to learn more.
@ -540,7 +578,7 @@ fun PoweredByPocketHeader(
@Composable
@Preview
private fun PocketStoriesComposablesPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer2)) {
Column {
PocketStories(

@ -27,7 +27,6 @@ import org.mozilla.fenix.components.components
import org.mozilla.fenix.compose.ComposeViewHolder
import org.mozilla.fenix.compose.home.HomeSectionHeader
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
import org.mozilla.fenix.wallpapers.WallpaperState
/**
@ -105,7 +104,7 @@ class PocketStoriesViewHolder(
@Composable
@Preview
fun PocketStoriesViewHolderPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
Column {
HomeSectionHeader(
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.home.recentbookmarks.RecentBookmark
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
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_NO)
private fun RecentBookmarksPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
RecentBookmarks(
bookmarks = listOf(
RecentBookmark(

@ -52,7 +52,6 @@ import org.mozilla.fenix.compose.button.SecondaryButton
import org.mozilla.fenix.home.recentsyncedtabs.RecentSyncedTab
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* A recent synced tab card.
@ -287,7 +286,7 @@ private fun LoadedRecentSyncedTab() {
url = "https://mozilla.org",
previewImageUrl = "https://mozilla.org",
)
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
RecentSyncedTab(
tab = tab,
onRecentSyncedTabClick = {},
@ -301,7 +300,7 @@ private fun LoadedRecentSyncedTab() {
@Preview
@Composable
private fun LoadingRecentSyncedTab() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
RecentSyncedTab(
tab = null,
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.RecentHistoryHighlight
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
// Number of recently visited items per column.
private const val VISITS_PER_COLUMN = 3
@ -416,7 +415,7 @@ private val LazyListState.atLeastHalfVisibleItems
@Composable
@Preview
private fun RecentlyVisitedPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
RecentlyVisited(
recentVisits = listOf(
RecentHistoryGroup(title = "running shoes"),

@ -40,6 +40,7 @@ import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
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.Mode
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.SumoTopic.PRIVATE_BROWSING_MYTHS
import org.mozilla.fenix.utils.Settings
@ -209,6 +212,11 @@ interface SessionControlController {
* @see [SessionControlInteractor.reportSessionMetrics]
*/
fun handleReportSessionMetrics(state: AppState)
/**
* @see [SearchSelectorInteractor.onMenuItemTapped]
*/
fun handleMenuItemTapped(item: SearchSelectorMenu.Item)
}
@Suppress("TooManyFunctions", "LargeClass", "LongParameterList")
@ -660,4 +668,26 @@ class DefaultSessionControlController(
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.controller.RecentVisitsController
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
/**
@ -270,7 +272,8 @@ class SessionControlInteractor(
RecentBookmarksInteractor,
RecentVisitsInteractor,
CustomizeHomeIteractor,
PocketStoriesInteractor {
PocketStoriesInteractor,
SearchSelectorInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
@ -485,4 +488,8 @@ class SessionControlInteractor(
override fun onMessageClosedClicked(message: 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.home.sessioncontrol.TabSessionInteractor
import org.mozilla.fenix.theme.FirefoxTheme
import org.mozilla.fenix.theme.Theme
/**
* View holder for a private browsing description.
@ -116,7 +115,7 @@ fun PrivateBrowsingDescription(
@Composable
@Preview
private fun PrivateBrowsingDescriptionPreview() {
FirefoxTheme(theme = Theme.getTheme()) {
FirefoxTheme {
PrivateBrowsingDescription(
onLearnMoreClick = {},
)

@ -31,6 +31,7 @@ import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.R
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.databinding.TopSiteItemBinding
import org.mozilla.fenix.ext.bitmapForUrl
import org.mozilla.fenix.ext.components
@ -67,9 +68,22 @@ class TopSiteItemViewHolder(
is TopSiteItemMenu.Item.RenameTopSite -> interactor.onRenameTopSiteClicked(
topSite,
)
is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked(
topSite,
)
is TopSiteItemMenu.Item.RemoveTopSite -> {
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.SponsorPrivacy -> interactor.onSponsorPrivacyClicked()
}

@ -39,6 +39,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
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.requireComponents
import org.mozilla.fenix.ext.setTextColor
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.utils.allowUndo

@ -70,6 +70,7 @@ class BookmarkSearchDialogFragment : AppCompatDialogFragment(), UserInteractionH
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun 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.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.support.ktx.kotlin.toShortUrl
import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
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.requireComponents
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.friendlyRootTitle

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

@ -70,6 +70,7 @@ class HistorySearchDialogFragment : AppCompatDialogFragment(), UserInteractionHa
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun 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.flowScoped
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.kotlin.toShortUrl
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
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.setTextColor
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.library.LibraryPageFragment
import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.historymetadata.controller.DefaultHistoryMetadataGroupController

@ -5,12 +5,9 @@
package org.mozilla.fenix.onboarding
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
@ -36,9 +33,15 @@ class DefaultBrowserNotificationWorker(
) : Worker(context, workerParameters) {
override fun doWork(): Result {
val channelId = ensureChannelExists()
val channelId = ensureMarketingChannelExists(applicationContext)
NotificationManagerCompat.from(applicationContext)
.notify(NOTIFICATION_TAG, NOTIFICATION_ID, buildNotification(channelId))
.notify(
NOTIFICATION_TAG,
DEFAULT_BROWSER_NOTIFICATION_ID,
buildNotification(channelId),
)
Events.defaultBrowserNotifShown.record(NoExtras())
// 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 {
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 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_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) =
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.ext.navigateSafe
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor
import org.mozilla.fenix.search.toolbar.SearchSelectorMenu
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Settings
@ -54,7 +55,7 @@ interface SearchController {
fun handleSearchEngineSuggestionClicked(searchEngine: SearchEngine)
/**
* @see [ToolbarInteractor.onMenuItemTapped]
* @see [SearchSelectorInteractor.onMenuItemTapped]
*/
fun handleMenuItemTapped(item: SearchSelectorMenu.Item)
}

@ -154,6 +154,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : Dialog(requireContext(), this.theme) {
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
this@SearchDialogFragment.onBackPressed()
}
@ -178,6 +179,9 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
tabId = args.sessionId,
pastedText = args.pastedText,
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,
)
}
requireContext().components.clipboardHandler.text = null
}
val stubListener = ViewStub.OnInflateListener { _, inflated ->
@ -447,7 +452,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
updateQrButton(it)
}
updateVoiceSearchButton(it)
updateVoiceSearchButton()
}
}
@ -518,6 +523,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
override fun onResume() {
super.onResume()
qrFeature.get()?.let {
if (it.isScanInProgress) {
it.scan(binding.searchWrapper.id)
}
}
view?.post {
// 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.
@ -754,15 +765,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
searchSelectorAlreadyAdded = true
}
private fun updateVoiceSearchButton(searchFragmentState: SearchFragmentState) {
val searchEngine = searchFragmentState.searchEngineSource.searchEngine
val isVisible =
searchEngine?.id?.contains("google") == true &&
isSpeechAvailable() &&
requireContext().settings().shouldShowVoiceSearch
when (isVisible) {
private fun updateVoiceSearchButton() {
when (isSpeechAvailable() && requireContext().settings().shouldShowVoiceSearch) {
true -> {
if (voiceSearchButtonAction == null) {
voiceSearchButtonAction = IncreasedTapAreaActionDecorator(

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

@ -8,6 +8,7 @@ import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.RelativeLayout
import org.mozilla.fenix.databinding.SearchSelectorBinding
@ -21,9 +22,21 @@ internal class SearchSelector @JvmOverloads constructor(
) : RelativeLayout(context, attrs, defStyle) {
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.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