Merge tag 'v94.1.2' into upstream-sync

pull/415/head
Adam Novak 3 years ago
commit a77b05cdc7

@ -11,14 +11,6 @@ jobs:
target-tasks-method: nightly
when:
- {hour: 5, minute: 0}
# This is a temporary hook in order to not overload Google Play.
# See bug 1628413 for more context.
- name: nightly-on-google-play
job:
type: decision-task
treeherder-symbol: Nd-gp
target-tasks-method: nightly-on-google-play
when:
- {hour: 17, minute: 0}
- name: fennec-production
job:

@ -107,7 +107,7 @@ jobs:
run-ui:
runs-on: macos-11
if: github.event.pull_request.head.repo.full_name != github.repository && github.actor != 'MickeyMoz'
if: ${{ false }} # disable for now'
timeout-minutes: 60
strategy:

@ -1,10 +1,11 @@
plugins {
id "com.jetbrains.python.envs" version "0.0.26"
id "com.google.protobuf" version "0.8.17"
}
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
@ -208,6 +209,7 @@ android {
}
beta {
java.srcDirs = ['src/migration/java']
manifest.srcFile "src/migration/AndroidManifest.xml"
}
release {
java.srcDirs = ['src/migration/java']
@ -244,6 +246,8 @@ android {
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
testOptions {
@ -254,7 +258,6 @@ android {
// reserve more memory and also create a new process after every 80 test classes. This
// is a band-aid solution and eventually we should try to find and fix the leaks
// instead. :)
maxParallelForks = 2
forkEvery = 80
maxHeapSize = "3072m"
minHeapSize = "1024m"
@ -270,19 +273,19 @@ android {
}
}
android.applicationVariants.all { variant ->
// -------------------------------------------------------------------------------------------------
// Set up kotlin-allopen plugin for writing tests
// -------------------------------------------------------------------------------------------------
boolean hasTest = gradle.startParameter.taskNames.find { it.contains("test") || it.contains("Test") } != null
if (hasTest) {
apply plugin: 'kotlin-allopen'
allOpen {
annotation("org.mozilla.fenix.utils.OpenClass")
}
boolean hasTest = gradle.startParameter.taskNames.find { it.contains("test") || it.contains("Test") } != null
if (hasTest) {
apply plugin: 'kotlin-allopen'
allOpen {
annotation("org.mozilla.fenix.utils.OpenClass")
}
}
android.applicationVariants.all { variant ->
// -------------------------------------------------------------------------------------------------
// Generate version codes for builds
@ -424,10 +427,6 @@ android.applicationVariants.all { variant ->
}
}
androidExtensions {
experimental = true
}
// Generate Kotlin code for the Fenix Glean metrics.
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
@ -464,6 +463,9 @@ dependencies {
implementation Deps.sentry
implementation Deps.mozilla_compose_awesomebar
implementation Deps.mozilla_concept_awesomebar
implementation Deps.mozilla_concept_base
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu
@ -473,8 +475,6 @@ dependencies {
implementation Deps.mozilla_concept_toolbar
implementation Deps.mozilla_concept_tabstray
implementation Deps.mozilla_browser_awesomebar
implementation Deps.mozilla_feature_downloads
implementation Deps.mozilla_browser_domains
implementation Deps.mozilla_browser_icons
implementation Deps.mozilla_browser_menu
@ -486,9 +486,7 @@ dependencies {
implementation Deps.mozilla_browser_thumbnails
implementation Deps.mozilla_browser_toolbar
implementation Deps.mozilla_support_extensions
implementation Deps.mozilla_feature_addons
implementation Deps.mozilla_feature_accounts
implementation Deps.mozilla_feature_app_links
implementation Deps.mozilla_feature_autofill
@ -521,22 +519,23 @@ dependencies {
implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter
implementation Deps.mozilla_service_pocket
implementation Deps.mozilla_service_digitalassetlinks
implementation Deps.mozilla_service_sync_autofill
implementation Deps.mozilla_service_sync_logins
implementation Deps.mozilla_service_firefox_accounts
implementation Deps.mozilla_service_glean
implementation(Deps.mozilla_service_glean)
implementation Deps.mozilla_service_location
implementation Deps.mozilla_service_nimbus
implementation Deps.mozilla_support_extensions
implementation Deps.mozilla_support_base
implementation Deps.mozilla_support_images
implementation Deps.mozilla_support_ktx
implementation Deps.mozilla_support_rustlog
implementation Deps.mozilla_support_utils
implementation Deps.mozilla_support_locale
implementation Deps.mozilla_support_migration
implementation Deps.mozilla_ui_colors
@ -563,21 +562,25 @@ dependencies {
implementation Deps.androidx_navigation_fragment
implementation Deps.androidx_navigation_ui
implementation Deps.androidx_recyclerview
implementation Deps.androidx_lifecycle_common
implementation Deps.androidx_lifecycle_livedata
implementation Deps.androidx_lifecycle_process
implementation Deps.androidx_lifecycle_runtime
implementation Deps.androidx_lifecycle_viewmodel
implementation Deps.androidx_core
implementation Deps.androidx_core_ktx
implementation Deps.androidx_transition
implementation Deps.androidx_work_ktx
implementation Deps.androidx_datastore
implementation Deps.protobuf_javalite
implementation Deps.google_material
androidTestImplementation Deps.uiautomator
// Removed pending AndroidX fixes
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
// This Falcon version is added to maven central now required for Screengrab
implementation 'com.jraska:falcon:2.2.0'
// androidTestImplementation "br.com.concretesolutions:kappuccino:1.2.1"
androidTestImplementation Deps.androidx_compose_ui_test
androidTestImplementation Deps.espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
@ -622,11 +625,30 @@ dependencies {
// For the initial release of Glean 19, we require consumer applications to
// depend on a separate library for unit tests. This will be removed in future releases.
testImplementation "org.mozilla.telemetry:glean-forUnitTests:${project.ext.glean_version}"
testImplementation "org.mozilla.telemetry:glean-native-forUnitTests:${project.ext.glean_version}"
lintChecks project(":mozilla-lint-rules")
}
protobuf {
protoc {
artifact = Deps.protobuf_compiler
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
if (project.hasProperty("coverage")) {
tasks.withType(Test).configureEach {
jacoco.includeNoLocationClasses = true

File diff suppressed because one or more lines are too long

@ -50,7 +50,11 @@
<!-- Performance: checks we'd like to eventually set to error. -->
<issue id="UseCompoundDrawables" severity="warning" />
<issue id="Overdraw" severity="warning" />
<issue id="UnusedResources" severity="error" />
<issue id="UnusedResources" severity="error">
<!-- Using an automated process to remove localized strings after they are removed from the default strings.xml
means the files for localized strings will contain unused resources for a few days after the original removal operation. -->
<ignore path="**/values-*/strings.xml" />
</issue>
<!-- Performance: checks that we're unsure of the value of that we might want to investigate. -->
<issue id="UnpackedNativeCode" severity="informational" />

File diff suppressed because it is too large Load Diff

@ -30,24 +30,3 @@ first-session:
- https://github.com/mozilla-mobile/fenix/pull/8074#issuecomment-586512202
notification_emails:
- android-probes@mozilla.com
startup-timeline:
description: |
This ping is intended to provide an understanding of startup performance.
In addition to being captured on real devices, the ping data was prematurely
optimized into this separate ping to be isolated from other metrics to be
more easily captured by performance testing automation but that hasn't
happened in practice. We would have removed it but implementation
details don't make that possible:
https://github.com/mozilla-mobile/fenix/issues/17972#issuecomment-781002987
include_client_id: true
bugs:
- https://github.com/mozilla-mobile/fenix/issues/8803
- https://github.com/mozilla-mobile/fenix/issues/17972
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626
- https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com

@ -5,6 +5,7 @@ object Constants {
object PackageName {
const val GOOGLE_PLAY_SERVICES = "com.android.vending"
const val GOOGLE_APPS_PHOTOS = "com.google.android.apps.photos"
const val YOUTUBE_APP = "com.google.android.youtube"
}
const val LONG_CLICK_DURATION: Long = 5000

@ -8,6 +8,7 @@ import android.app.PendingIntent
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
@ -21,11 +22,14 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
@ -33,6 +37,7 @@ import kotlinx.coroutines.runBlocking
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -190,4 +195,40 @@ object TestHelper {
canvas.drawColor(Color.GREEN)
return bitmap
}
fun isPackageInstalled(packageName: String): Boolean {
return try {
val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (exception: PackageManager.NameNotFoundException) {
false
}
}
fun assertExternalAppOpens(appPackageName: String) {
if (isPackageInstalled(appPackageName)) {
Intents.intended(IntentMatchers.toPackage(appPackageName))
} else {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
Until.findObject(By.text("Could not open file")),
waitingTime
)
}
}
fun returnToBrowser() {
val urlBar =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_url_view"))
do {
mDevice.pressBack()
} while (
!urlBar.waitForExists(waitingTime)
)
}
fun UiDevice.waitForObjects(obj: UiObject, waitingTime: Long = TestAssetHelper.waitingTime) {
this.waitForIdle()
Assert.assertNotNull(obj.waitForExists(waitingTime))
}
}

@ -1,36 +0,0 @@
package org.mozilla.fenix.helpers.assertions
import android.view.View
import androidx.test.espresso.ViewAssertion
import mozilla.components.browser.awesomebar.BrowserAwesomeBar
class AwesomeBarAssertion {
companion object {
fun suggestionsAreGreaterThan(minimumSuggestions: Int): ViewAssertion {
return ViewAssertion { view, noViewFoundException ->
if (noViewFoundException != null) throw noViewFoundException
val suggestionsCount = getSuggestionCountFromView(view)
if (suggestionsCount <= minimumSuggestions)
throw AssertionError("The suggestion count is less than or equal to the minimum suggestions")
}
}
fun suggestionsAreEqualTo(expectedItemCount: Int): ViewAssertion {
return ViewAssertion { view, noViewFoundException ->
if (noViewFoundException != null) throw noViewFoundException
val suggestionsCount = getSuggestionCountFromView(view)
if (suggestionsCount != expectedItemCount)
throw AssertionError("The expected item count is $expectedItemCount, and the suggestions count within the AwesomeBar is $suggestionsCount")
}
}
private fun getSuggestionCountFromView(view: View): Int {
return (view as BrowserAwesomeBar).adapter?.itemCount
?: throw AssertionError("This view is not of type BrowserAwesomeBar")
}
}
}

@ -4,26 +4,26 @@
package org.mozilla.fenix.perf
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.android.synthetic.main.activity_home.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.HomeActivityTestRule
// BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM.
private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val EXPECTED_RUNBLOCKING_COUNT = 3
private const val EXPECTED_COMPONENT_INIT_COUNT = 42
private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_SUPPRESSION_COUNT = 19
@Suppress("TopLevelPropertyNaming") // it's silly this would have a different naming convention b/c no const
private val EXPECTED_RUNBLOCKING_RANGE = 0..1 // CI has +1 counts compared to local runs: increment these together
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4
private const val EXPECTED_NUMBER_OF_INFLATION = 12
@ -37,17 +37,6 @@ private val failureMsgRunBlocking = getErrorMessage(
implications = "using runBlocking may block the main thread and have other negative performance implications?"
)
private val failureMsgComponentInit = getErrorMessage(
shortName = "Component init",
implications = "initializing new components on start up may be an indication that we're doing more work than necessary on start up?"
)
private val failureMsgViewHierarchyDepth = getErrorMessage(
shortName = "view hierarchy depth",
implications = "having a deep view hierarchy can slow down measure/layout performance?"
) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " +
"we'll find out over time if it is or is not."
private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
shortName = "ConstraintLayout being a common direct descendant of a RecyclerView",
implications = "ConstraintLayouts are slow to inflate and are primarily used to flatten deep " +
@ -90,18 +79,14 @@ class StartupExcessiveResourceUseTest {
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actualSuppresionCount = activityTestRule.activity.components.strictMode.suppressionCount.get().toInt()
val actualRunBlocking = RunBlockingCounter.count.get()
val actualComponentInitCount = ComponentInitCount.count.get()
val rootView = activityTestRule.activity.rootContainer
val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1)
val rootView = activityTestRule.activity.findViewById<LinearLayout>(R.id.rootContainer)
val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
val actualNumberOfInflations = InflationCounter.inflationCount.get()
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount)
assertEquals(failureMsgViewHierarchyDepth, EXPECTED_VIEW_HIERARCHY_DEPTH, actualViewHierarchyDepth)
assertTrue(failureMsgRunBlocking + "actual: $actualRunBlocking", actualRunBlocking in EXPECTED_RUNBLOCKING_RANGE)
assertEquals(
failureMsgRecyclerViewConstraintLayoutChildren,
EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
@ -111,19 +96,6 @@ class StartupExcessiveResourceUseTest {
}
}
private fun countAndLogViewHierarchyDepth(view: View, level: Int): Int {
// Log for debugging purposes: not sure if this is actually helpful.
val indent = "| ".repeat(level - 1)
Log.d("Startup...Test", "${indent}$view")
return if (view !is ViewGroup) {
level
} else {
val maxDepth = view.children.map { countAndLogViewHierarchyDepth(it, level + 1) }.maxOrNull()
maxDepth ?: level
}
}
private fun countRecyclerViewConstraintLayoutChildren(view: View, parent: View?): Int {
val viewValue = if (parent is RecyclerView && view is ConstraintLayout) {
1

@ -16,6 +16,7 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.bookmarkStorage
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
@ -51,6 +52,8 @@ class BookmarksTest {
dispatcher = AndroidAssetDispatcher()
start()
}
val settings = activityTestRule.activity.settings()
settings.shouldShowJumpBackInCFR = false
}
@After
@ -68,6 +71,25 @@ class BookmarksTest {
}
}
@Test
fun verifyEmptyBookmarksMenuTest() {
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(
activityTestRule.activity.findViewById(R.id.bookmark_list),
1
)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarksMenuView()
verifyAddFolderButton()
verifyCloseButton()
verifyBookmarkTitle("Desktop Bookmarks")
}
}
@Test
fun defaultDesktopBookmarksFoldersTest() {
homeScreen {
@ -187,6 +209,15 @@ class BookmarksTest {
}.openThreeDotMenu(defaultWebPage.url) {
}.clickCopy {
verifyCopySnackBarText()
navigateUp()
}
navigationToolbar {
}.clickUrlbar {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyPastedToolbarText(defaultWebPage.url.toString())
}
}
@ -316,6 +347,8 @@ class BookmarksTest {
@Test
fun openSelectionInNewTabTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
@ -394,6 +427,38 @@ class BookmarksTest {
}
}
@Test
fun undoDeleteMultipleSelectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
browserScreen {
createBookmark(firstWebPage.url)
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 3)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}
multipleSelectionToolbar {
clickMultiSelectionDelete()
}
bookmarksMenu {
verifyDeleteMultipleBookmarksSnackBar()
clickUndoDeleteButton()
verifyBookmarkedURL(firstWebPage.url.toString())
verifyBookmarkedURL(secondWebPage.url.toString())
}
}
@Test
fun multipleSelectionShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -432,12 +497,12 @@ class BookmarksTest {
}.openThreeDotMenu("1") {
}.clickDelete {
verifyDeleteFolderConfirmationMessage()
confirmFolderDeletion()
confirmDeletion()
verifyDeleteSnackBarText()
}.openThreeDotMenu("2") {
}.clickDelete {
verifyDeleteFolderConfirmationMessage()
confirmFolderDeletion()
confirmDeletion()
verifyDeleteSnackBarText()
verifyFolderTitle("3")
}.closeMenu {
@ -528,4 +593,24 @@ class BookmarksTest {
verifyHomeScreen()
}
}
@Test
fun deleteBookmarkInEditModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickEdit {
clickDeleteInEditModeButton()
confirmDeletion()
verifyDeleteSnackBarText()
}
}
}

@ -12,6 +12,7 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
@ -39,6 +40,8 @@ class CollectionTest {
@Before
fun setUp() {
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -78,6 +81,7 @@ class CollectionTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -104,6 +108,7 @@ class CollectionTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)
@ -153,8 +158,10 @@ class CollectionTest {
}.openTabDrawer {
createCollection(webPage.title, firstCollectionName)
verifySnackBarText("Collection saved!")
}.closeTabDrawer {
}.goToHomescreen {
closeTab()
}
homeScreen {
}.expandCollection(firstCollectionName) {
removeTabFromCollection(webPage.title)
verifyTabSavedInCollection(webPage.title, false)
@ -166,7 +173,6 @@ class CollectionTest {
}
@Test
@Ignore("To be fixed in https://github.com/mozilla-mobile/fenix/issues/20702")
fun swipeToRemoveTabFromCollectionTest() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -184,10 +190,13 @@ class CollectionTest {
}.openThreeDotMenu {
}.openSaveToCollection {
}.selectExistingCollection(firstCollectionName) {
}.goToHomescreen {}
}.openTabDrawer {
closeTab()
}
homeScreen {
}.expandCollection(firstCollectionName) {
swipeToBottom()
swipeCollectionItemLeft(firstWebPage.title)
verifyTabSavedInCollection(firstWebPage.title, false)
swipeCollectionItemRight(secondWebPage.title)
@ -209,6 +218,7 @@ class CollectionTest {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.openTabDrawer {
longClickTab(firstWebPage.title)
verifyTabsMultiSelectionCounter(1)

@ -7,6 +7,7 @@ package org.mozilla.fenix.ui
import android.content.Context
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.IdlingRegistry
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import okhttp3.mockwebserver.MockWebServer
@ -15,6 +16,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
@ -40,6 +42,9 @@ class HistoryTest {
@Before
fun setUp() {
InstrumentationRegistry.getInstrumentation().targetContext.settings()
.shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -95,87 +100,6 @@ class HistoryTest {
}
}
@Test
fun copyHistoryItemURLTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu {
}.clickCopy {
verifyCopySnackBarText()
}
}
@Test
fun shareHistoryItemTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu {
}.clickShare {
verifyShareOverlay()
verifyShareTabFavicon()
verifyShareTabTitle()
verifyShareTabUrl()
}
}
@Test
fun openHistoryItemInNewTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu {
}.clickOpenInNormalTab {
verifyTabTrayIsOpened()
verifyNormalModeSelected()
}
}
@Test
fun openHistoryItemInNewPrivateTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu {
}.clickOpenInPrivateTab {
verifyTabTrayIsOpened()
verifyPrivateModeSelected()
}
}
@Test
fun deleteHistoryItemTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -189,9 +113,8 @@ class HistoryTest {
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
}.openThreeDotMenu {
clickDeleteHistoryButton()
IdlingRegistry.getInstance().unregister(historyListIdlingResource!!)
}.clickDelete {
verifyDeleteSnackbarText("Deleted")
verifyEmptyHistoryView()
}
@ -210,7 +133,7 @@ class HistoryTest {
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
clickDeleteHistoryButton()
clickDeleteAllHistoryButton()
IdlingRegistry.getInstance().unregister(historyListIdlingResource!!)
verifyDeleteConfirmationMessage()
confirmDeleteAllHistory()
@ -304,15 +227,18 @@ class HistoryTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
mDevice.waitForIdle()
verifyUrl(secondWebPage.url.toString())
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 2)
IdlingRegistry.getInstance().register(historyListIdlingResource!!)
verifyHistoryItemExists(firstWebPage.url.toString())
verifyHistoryItemExists(secondWebPage.url.toString())
longTapSelectItem(firstWebPage.url)
longTapSelectItem(secondWebPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)

@ -4,15 +4,16 @@
package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.HomeActivityTestRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.homeScreen
/**
@ -85,4 +86,57 @@ class HomeScreenTest {
verifyHomeComponent()
}
}
@Test
fun dismissOnboardingUsingSettingsTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openSettings {
verifyBasicsHeading()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingBookmarksTest() {
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
navigateUp()
}
homeScreen {
verifyExistingTopSitesList()
}
}
@Test
fun dismissOnboardingUsingHelpTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
homeScreen {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.goBack {
verifyExistingTopSitesList()
}
}
@Test
fun toolbarTapDoesntDismissOnboardingTest() {
homeScreen {
verifyWelcomeHeader()
}.openSearch {
verifyScanButton()
verifySearchEngineButton()
verifyKeyboardVisibility()
}.dismissSearchBar {
verifyWelcomeHeader()
}
}
}

@ -12,6 +12,7 @@ import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -41,6 +42,8 @@ class NavigationToolbarTest {
dispatcher = AndroidAssetDispatcher()
start()
}
val settings = activityTestRule.activity.settings()
settings.shouldShowJumpBackInCFR = false
}
@After

@ -5,10 +5,12 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
@ -53,6 +55,8 @@ class NoNetworkAccessStartupTests {
// Based on STR from https://github.com/mozilla-mobile/fenix/issues/16886
fun networkInterruptedFromBrowserToHomeTest() {
val url = "example.com"
val settings = InstrumentationRegistry.getInstrumentation().targetContext.settings()
settings.shouldShowJumpBackInCFR = false
activityTestRule.launchActivity(null)

@ -4,6 +4,7 @@
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@ -23,7 +24,10 @@ import org.mozilla.fenix.ui.robots.homeScreen
class SearchTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@get:Rule
val activityTestRule = HomeActivityTestRule()
val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule(),
{ it.activity }
)
@Test
fun searchScreenItemsTest() {
@ -58,11 +62,11 @@ class SearchTest {
}.goBack {
}.goBack {
}.openSearch {
// verifySearchWithText()
clickSearchEngineButton("DuckDuckGo")
verifySearchBarEmpty()
clickSearchEngineButton(activityTestRule, "DuckDuckGo")
typeSearch("mozilla")
verifySearchEngineResults("DuckDuckGo")
clickSearchEngineResult("DuckDuckGo")
verifySearchEngineResults(activityTestRule, "DuckDuckGo", 4)
clickSearchEngineResult(activityTestRule, "DuckDuckGo")
verifySearchEngineURL("DuckDuckGo")
}
}
@ -77,8 +81,8 @@ class SearchTest {
}.goBack {
}.goBack {
}.openSearch {
scrollToSearchEngineSettings()
clickSearchEngineSettings()
scrollToSearchEngineSettings(activityTestRule)
clickSearchEngineSettings(activityTestRule)
verifySearchSettings()
}
}

@ -12,6 +12,7 @@ import org.junit.Rule
import org.junit.Before
import org.junit.After
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.ui.robots.clickRateButtonGooglePlay
@ -75,6 +76,8 @@ class SettingsAboutTest {
@Test
fun verifyAboutFirefoxPreview() {
val settings = activityIntentTestRule.activity.settings()
settings.shouldShowJumpBackInCFR = false
homeScreen {
}.openThreeDotMenu {
}.openSettings {

@ -14,6 +14,7 @@ import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FenixApplication
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
@ -42,6 +43,8 @@ class SettingsBasicsTest {
dispatcher = AndroidAssetDispatcher()
start()
}
val settings = activityIntentTestRule.activity.settings()
settings.shouldShowJumpBackInCFR = false
}
@After
@ -150,22 +153,13 @@ class SettingsBasicsTest {
}
}
@Test
fun changeCloseTabsSetting() {
// Goes through the settings and verified the close tabs setting options.
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
verifyOptions()
}
}
@Test
fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.
val fenixApp = activityIntentTestRule.activity.applicationContext as FenixApplication
val webpage = getLoremIpsumAsset(mockWebServer).url
val settings = fenixApp.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
// This value will represent the text size percentage the webpage will scale to. The default value is 100%.
val textSizePercentage = 180
@ -183,9 +177,6 @@ class SettingsBasicsTest {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(webpage) {
checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.openThreeDotMenu {
}.openSettings {
}.openAccessibilitySubMenu {

@ -9,9 +9,9 @@ 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.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -22,6 +22,7 @@ import org.mozilla.fenix.ui.robots.addToHomeScreen
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsScreen
/**
* Tests for verifying the main three dot menu options
@ -44,6 +45,9 @@ class SettingsPrivacyTest {
dispatcher = AndroidAssetDispatcher()
start()
}
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
}
@After
@ -200,9 +204,8 @@ class SettingsPrivacyTest {
verifySaveLoginPromptIsShown()
// Click save to save the login
saveLoginFromPrompt("Save")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}
browserScreen {
}.openThreeDotMenu {
}.openSettings {
TestHelper.scrollToElementByText("Logins and passwords")
@ -219,15 +222,14 @@ class SettingsPrivacyTest {
@Test
fun neverSaveLoginFromPromptTest() {
val saveLoginTest = TestAssetHelper.getSaveLoginAsset(mockWebServer)
val settings = activityTestRule.activity.settings()
settings.shouldShowJumpBackInCFR = false
navigationToolbar {
}.enterURLAndEnterToBrowser(saveLoginTest.url) {
verifySaveLoginPromptIsShown()
// Don't save the login, add to exceptions
saveLoginFromPrompt("Never save")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.openThreeDotMenu {
}.openSettings {
}.openLoginsAndPasswordSubMenu {
@ -327,6 +329,8 @@ class SettingsPrivacyTest {
@Test
fun launchLinksInPrivateToggleOffStateDoesntChangeTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
setOpenLinksInPrivateOn()
@ -364,6 +368,7 @@ class SettingsPrivacyTest {
}.openThreeDotMenu {
}.openSettings {
}.openPrivateBrowsingSubMenu {
cancelPrivateShortcutAddition()
addPrivateShortcutToHomescreen()
verifyPrivateBrowsingShortcutIcon()
}.openPrivateBrowsingShortcut {
@ -374,175 +379,140 @@ class SettingsPrivacyTest {
}
}
@Ignore("This is a stub test, ignore for now")
@Test
fun toggleTrackingProtection() {
// Open static test website to verify TP is turned on (default): https://github.com/rpappalax/testapp
// (static content needs to be migrated to assets folder)
// Open 3dot (main) menu
// Select settings
// Toggle Tracking Protection to 'off'
// Back arrow to Home
// Open static test website to verify TP is now off: https://github.com/rpappalax/testapp
}
fun deleteBrowsingDataOptionStatesTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
switchBrowsingHistoryCheckBox()
switchCachedFilesCheckBox()
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
}
@Ignore("This is a stub test, ignore for now")
@Test
fun verifySitePermissions() {
// Open 3dot (main) menu
// Select settings
// Click on: "Site permissions"
// Verify sub-menu items...
// Click on: "Exceptions"
// Verify: "No site exceptions"
// TBD: create a site exception
// TBD: return to this UI and verify
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: geolocation"
// Verify that geolocation permissions dialogue is opened
// Verify text: "Allow <website URL> to use your geolocation?
// Verify toggle: 'Remember decision for this site?"
// Verify button: "Don't Allow"
// Verify button: "Allow" (default)
// Select "Remember decision for this site"
// Refresh page
// Click on "Test site permissions: geolocation"
// Verify that geolocation permissions dialogue is not opened
//
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: camera"
// Verify that camera permissions dialogue is opened
// Verify text: "Allow <website URL> to use your camera?
// Verify toggle: 'Remember decision for this site?"
// Verify button: "Don't Allow"
// Verify button: "Allow" (default)
// Select "Remember decision for this site"
// Refresh page
// Click on "Test site permissions: camera"
// Verify that camera permissions dialogue is not opened
//
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: microphone"
// Verify that microphone permissions dialogue is opened
// Verify text: "Allow <website URL> to use your microphone?
// Verify toggle: 'Remember decision for this site?"
// Verify button: "Don't Allow"
// Verify button: "Allow" (default)
// Select "Remember decision for this site"
// Refresh page
// Click on "Test site permissions: microphone"
// Verify that microphone permissions dialogue is not opened
//
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: notifications dialogue"
// Verify that notifications dialogue permissions dialogue is opened
// Verify text: "Allow <website URL> to send notifications?
// Verify toggle: 'Remember decision for this site?"
// Verify button: "Never"
// Verify button: "Always" (default)
// Select "Remember decision for this site"
// Refresh page
// Click on "Test site permissions: notifications dialogue"
// Verify that notifications dialogue permissions dialogue is not opened
//
// Open 3dot (main) menu
// Select settings
// Click on: "Site permissions"
// Select: Camera
// Switch from "ask to allow" (default) to "blocked"
// Click back arrow
//
// Select: Location
// Switch from "ask to allow" (default) to "blocked"
// Click back arrow
//
// Select: Microphone
// Switch from "ask to allow" (default) to "blocked"
// Click back arrow
//
// Select: Notification
// Switch from "ask to allow" (default) to "blocked"
// Click back arrow
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: camera dialogue"
// Verify that notifications dialogue permissions dialogue is not opened
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: geolocation dialogue"
// Verify that notifications dialogue permissions dialogue is not opened
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: microphone dialogue"
// Verify that notifications dialogue permissions dialogue is not opened
//
// Open browser to static test website: https://github.com/rpappalax/testapp
// Click on "Test site permissions: notifications dialogue"
// Verify that notifications dialogue permissions dialogue is not opened
}
restartApp(activityTestRule)
@Ignore("This is a stub test, ignore for now")
@Test
fun deleteBrowsingData() {
// Setup:
// Open 2 websites as 2 tabs
// Save as 1 collection
// Open 2 more websites in 2 other tabs
// Save as a 2nd collection
// Open 3dot (main) menu
// Select settings
// Click on "Delete browsing data"
// Verify correct number of tabs, addresses and collections are indicated
// Select all 3 checkboxes
// Click on "Delete browsing data button"
// Return to home screen and verify that all tabs, history and collection are gone
//
// Verify xxx
//
// New: If coming from tab -> settings -> delete browsing data
// then expect to return to home screen
// If coming from tab -> home -> settings -> delete browsing data
// then expect return to settings (after which you can return to home manually)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(true)
verifyBrowsingHistoryDetails(false)
verifyCookiesCheckBox(true)
verifyCachedFilesCheckBox(false)
verifySitePermissionsCheckBox(true)
verifyDownloadsCheckBox(true)
switchOpenTabsCheckBox()
switchBrowsingHistoryCheckBox()
switchCookiesCheckBox()
switchCachedFilesCheckBox()
switchSitePermissionsCheckBox()
switchDownloadsCheckBox()
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
restartApp(activityTestRule)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsCheckBox(false)
verifyBrowsingHistoryDetails(true)
verifyCookiesCheckBox(false)
verifyCachedFilesCheckBox(true)
verifySitePermissionsCheckBox(false)
verifyDownloadsCheckBox(false)
}
}
@Ignore("This is a stub test, ignore for now")
@Test
fun verifyDataCollection() {
// Open 3dot (main) menu
// Select settings
// Click on "Data collection"
// Verify header: "Usage and technical data"
// Verify text: "Shares performance, usage, hardware and customization data about your browser with Mozilla"
// " to help us make Firefox preview better"
// Verify toggle is on by default
// TBD:
// see: telemetry testcases
fun deleteTabsDataWithNoOpenTabsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyBasicsHeading()
}
}
@Ignore("This is a stub test, ignore for now")
@Test
fun openPrivacyNotice() {
// Open 3dot (main) menu
// Select settings
// Click on "Privacy notice"
// Verify redirect to: mozilla.org Privacy notice page"
fun deleteTabsDataTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyAllCheckBoxesAreChecked()
selectOnlyOpenTabsCheckBox()
clickDeleteBrowsingDataButton()
clickDialogCancelButton()
verifyOpenTabsCheckBox(true)
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyBasicsHeading()
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsDetails("0")
}.goBack {
}.goBack {
}.openTabDrawer {
verifyNoTabsOpened()
}
}
@Ignore("This is a stub test, ignore for now")
@Test
fun checkLeakCanary() {
// Open 3dot (main) menu
// Select settings
// Click on Leak Canary toggle
// Verify 'dump' message
fun deleteDeleteBrowsingHistoryDataTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openSettingsSubMenuDeleteBrowsingData {
verifyBrowsingHistoryDetails("2")
selectOnlyBrowsingHistoryCheckBox()
clickDeleteBrowsingDataButton()
clickDialogCancelButton()
verifyBrowsingHistoryDetails(true)
clickDeleteBrowsingDataButton()
confirmDeletionAndAssertSnackbar()
verifyBrowsingHistoryDetails("0")
}.goBack {
verifyBasicsHeading()
}.goBack {
}
navigationToolbar {
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
}
}
}

@ -5,8 +5,8 @@
package org.mozilla.fenix.ui
import android.view.View
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.IdlingRegistry
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
@ -25,22 +25,24 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.createCustomTabIntent
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage
import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled
import org.mozilla.fenix.helpers.TestHelper.restartApp
import org.mozilla.fenix.helpers.TestHelper.returnToBrowser
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.clickTabCrashedRestoreButton
import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.collectionRobot
import org.mozilla.fenix.ui.robots.customTabScreen
import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen
@ -63,7 +65,6 @@ class SmokeTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
private var awesomeBar: ViewVisibilityIdlingResource? = null
private var searchSuggestionsIdlingResource: RecyclerViewIdlingResource? = null
private var addonsListIdlingResource: RecyclerViewIdlingResource? = null
private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null
private var readerViewNotification: ViewVisibilityIdlingResource? = null
@ -82,10 +83,14 @@ class SmokeTest {
return searchDialogFragment?.view?.findViewById(R.id.awesome_bar)
}
@get:Rule
val activityTestRule = HomeActivityIntentTestRule()
private lateinit var browserStore: BrowserStore
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityIntentTestRule(),
{ it.activity }
)
@get: Rule
val intentReceiverActivityTestRule = ActivityTestRule(
IntentReceiverActivity::class.java, true, false
@ -103,6 +108,7 @@ class SmokeTest {
// So we are initializing this here instead of in all related tests.
browserStore = activityTestRule.activity.components.core.store
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -117,10 +123,6 @@ class SmokeTest {
IdlingRegistry.getInstance().unregister(awesomeBar!!)
}
if (searchSuggestionsIdlingResource != null) {
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}
if (addonsListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!)
}
@ -239,10 +241,13 @@ class SmokeTest {
@Test
// Verifies the History menu opens from a tab's 3 dot menu
fun openMainMenuHistoryItemTest() {
homeScreen {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
verifyHistoryListExists()
}
}
@ -250,7 +255,10 @@ class SmokeTest {
@Test
// Verifies the Bookmarks menu opens from a tab's 3 dot menu
fun openMainMenuBookmarksItemTest() {
homeScreen {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
@ -261,10 +269,14 @@ class SmokeTest {
// Verifies the Synced tabs menu or Sync Sign In menu opens from a tab's 3 dot menu.
// The test is assuming we are NOT signed in.
fun openMainMenuSyncItemTest() {
homeScreen {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSyncSignIn {
verifySyncSignInMenuHeader()
verifyTurnOnSyncMenu()
}
}
@ -273,7 +285,10 @@ class SmokeTest {
// caution when making changes to it, so they don't block the builds
// Verifies the Settings menu opens from a tab's 3 dot menu
fun openMainMenuSettingsItemTest() {
homeScreen {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
@ -296,6 +311,9 @@ class SmokeTest {
@Test
// Verifies the Add to top sites option in a tab's 3 dot menu
fun openMainMenuAddTopSiteTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -304,9 +322,7 @@ class SmokeTest {
expandMenu()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesTabs(defaultWebPage.title)
}
}
@ -363,6 +379,37 @@ class SmokeTest {
}
}
@Test
// Verifies the Open in app button when an app is installed
fun mainMenuOpenInAppTest() {
val youtubeUrl = "m.youtube.com"
if (isPackageInstalled(YOUTUBE_APP)) {
navigationToolbar {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickOpenInApp {
assertExternalAppOpens(YOUTUBE_APP)
returnToBrowser()
verifyUrl(youtubeUrl)
}
}
}
@Test
// Verifies the Desktop site toggle in a tab's 3 dot menu
fun mainMenuDesktopSiteTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.switchDesktopSiteMode {
}.openThreeDotMenu {
verifyDesktopSiteModeEnabled(true)
}
}
@Test
// Verifies the Share button in a tab's 3 dot menu
fun mainMenuShareButtonTest() {
@ -391,34 +438,6 @@ class SmokeTest {
}
}
@Test
// Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar
fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
switchEnhancedTrackingProtectionToggle()
verifyEnhancedTrackingProtectionOptionsGrayedOut()
}.goBackToHomeScreen {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
verifyEnhancedTrackingProtectionPanelNotVisible()
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
switchEnhancedTrackingProtectionToggle()
}.goBack {
}.goBackToBrowser {
clickEnhancedTrackingProtectionPanel()
verifyEnhancedTrackingProtectionSwitch()
clickEnhancedTrackingProtectionSwitchOffOn()
}
}
}
@Test
fun customTrackingProtectionSettingsTest() {
val genericWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -437,16 +456,16 @@ class SmokeTest {
// browsing a basic page to allow GV to load on a fresh run
}.enterURLAndEnterToBrowser(genericWebPage.url) {
}.openNavigationToolbar {
}.openTrackingProtectionTestPage(trackingPage.url, true) {
dismissTrackingOnboarding()
}
}.enterURLAndEnterToBrowser(trackingPage.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
}.openDetails {
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
verifyFingerprintersBlocked()
verifyBasicLevelTrackingContentBlocked()
verifyTrackingContentBlocked()
viewTrackingContentBlockList()
}
}
@ -459,43 +478,47 @@ class SmokeTest {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
verifySearchEngineList()
changeDefaultSearchEngine("Amazon.com")
verifySearchEngineList(activityTestRule)
changeDefaultSearchEngine(activityTestRule, "Amazon.com")
verifySearchEngineIcon("Amazon.com")
}.goToSearchEngine {
mDevice.waitForIdle()
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineShortcutButton()
mDevice.waitForIdle()
changeDefaultSearchEngine("Bing")
changeDefaultSearchEngine(activityTestRule, "Bing")
verifySearchEngineIcon("Bing")
}.goToSearchEngine {
mDevice.waitForIdle()
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineShortcutButton()
mDevice.waitForIdle()
changeDefaultSearchEngine("DuckDuckGo")
changeDefaultSearchEngine(activityTestRule, "DuckDuckGo")
verifySearchEngineIcon("DuckDuckGo")
}.goToSearchEngine {
mDevice.waitForIdle()
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
}.openNewTab {
clickSearchEngineShortcutButton()
changeDefaultSearchEngine("Wikipedia")
changeDefaultSearchEngine(activityTestRule, "Wikipedia")
verifySearchEngineIcon("Wikipedia")
}.goToSearchEngine {
mDevice.waitForIdle()
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabDrawer {
// Checking whether the next search will be with default or not
}.openNewTab {
}.goToSearchEngine {
mDevice.waitForIdle()
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openNavigationToolbar {
clickUrlbar {
verifyDefaultSearchEngine("Google")
}
}.clickUrlbar {
verifyDefaultSearchEngine("Google")
}
}
@ -515,10 +538,45 @@ class SmokeTest {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains("YouTube")
mDevice.waitForIdle()
activityTestRule.waitForIdle()
verifyEnginesListShortcutContains(activityTestRule, "YouTube")
}
}
@Ignore("Started failing: https://github.com/mozilla-mobile/fenix/issues/21540")
@Test
// Verifies setting as default a customized search engine name and URL
fun editCustomSearchEngineTest() {
val searchEngine = object {
var title = "Elefant"
var url = "https://www.elefant.ro/search?SearchTerm=%s"
var newTitle = "Test"
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
typeCustomEngineDetails(searchEngine.title, searchEngine.url)
saveNewSearchEngine()
openEngineOverflowMenu(searchEngine.title)
clickEdit()
typeCustomEngineDetails(searchEngine.newTitle, searchEngine.url)
saveEditSearchEngine()
changeDefaultSearchEngine(searchEngine.newTitle)
}.goBack {
}.goBack {
}.openSearch {
verifyDefaultSearchEngine(searchEngine.newTitle)
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains(activityTestRule, searchEngine.newTitle)
}
}
@Ignore("Strated failing on Nighlty task: https://github.com/mozilla-mobile/fenix/issues/21620")
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
@ -526,32 +584,19 @@ class SmokeTest {
fun toggleSearchSuggestions() {
homeScreen {
}.openNavigationToolbar {
typeSearchTerm("mozilla")
val awesomeBarView = getAwesomebarView()
awesomeBarView?.let {
awesomeBar = ViewVisibilityIdlingResource(it, View.VISIBLE)
}
IdlingRegistry.getInstance().register(awesomeBar!!)
searchSuggestionsIdlingResource =
RecyclerViewIdlingResource(awesomeBarView as RecyclerView, 1)
IdlingRegistry.getInstance().register(searchSuggestionsIdlingResource!!)
verifySearchSuggestionsAreMoreThan(0)
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}.goBack {
}.openSearch {
typeSearch("mozilla")
verifySearchEngineSuggestionResults(activityTestRule, "mozilla firefox")
}.dismissSearchBar {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
disableShowSearchSuggestions()
}.goBack {
}.goBack {
}.openNavigationToolbar {
typeSearchTerm("mozilla")
searchSuggestionsIdlingResource =
RecyclerViewIdlingResource(getAwesomebarView() as RecyclerView)
IdlingRegistry.getInstance().register(searchSuggestionsIdlingResource!!)
verifySearchSuggestionsAreEqualTo(0)
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}.openSearch {
typeSearch("mozilla")
verifyNoSuggestionsAreDisplayed(activityTestRule, "mozilla firefox")
}
}
@ -575,7 +620,6 @@ class SmokeTest {
@Test
// Saves a login, then changes it and verifies the update
@Ignore("To be fixed in https://github.com/mozilla-mobile/fenix/issues/20702")
fun updateSavedLoginTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -659,14 +703,13 @@ class SmokeTest {
IdlingRegistry.getInstance().register(addonsListIdlingResource!!)
clickInstallAddon(addonName)
acceptInstallAddon()
verifyDownloadAddonPrompt(addonName, activityTestRule)
verifyDownloadAddonPrompt(addonName, activityTestRule.activityRule)
IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!)
}.goBack {
}.openNavigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionPage.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
}.enterURLAndEnterToBrowser(trackingProtectionPage.url) {
verifyPageContent(trackingProtectionPage.content)
}
}
@Test
@ -693,89 +736,6 @@ class SmokeTest {
}
}
@Test
// Verifies the items from the overflow menu of Recently Closed Tabs
fun recentlyClosedTabsMenuItemsTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuCopy()
verifyRecentlyClosedTabsMenuShare()
verifyRecentlyClosedTabsMenuNewTab()
verifyRecentlyClosedTabsMenuPrivateTab()
verifyRecentlyClosedTabsMenuDelete()
}
}
@Test
// Verifies the Copy option from the Recently Closed Tabs overflow menu
fun copyRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuCopy()
clickCopyRecentlyClosedTabs()
verifyCopyRecentlyClosedTabsSnackBarText()
}
}
@Test
// Verifies the Share option from the Recently Closed Tabs overflow menu
fun shareRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuShare()
clickShareRecentlyClosedTabs()
verifyShareOverlay()
verifyShareTabTitle("Test_Page_1")
verifyShareTabUrl(website.url)
verifyShareTabFavicon()
}
}
@Test
// Verifies the Open in a new tab option from the Recently Closed Tabs overflow menu
fun openRecentlyClosedTabsInNewTabTest() {
@ -795,8 +755,6 @@ class SmokeTest {
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuNewTab()
}.clickOpenInNewTab {
verifyUrl(website.url.toString())
}.openTabDrawer {
@ -805,35 +763,7 @@ class SmokeTest {
}
@Test
// Verifies the Open in a private tab option from the Recently Closed Tabs overflow menu
fun openRecentlyClosedTabsInNewPrivateTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuPrivateTab()
}.clickOpenInPrivateTab {
verifyUrl(website.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
@Test
// Verifies the delete option from the Recently Closed Tabs overflow menu
// Verifies the delete button from the Recently Closed Tabs
fun deleteRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -851,9 +781,7 @@ class SmokeTest {
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuDelete()
clickDeleteCopyRecentlyClosedTabs()
clickDeleteRecentlyClosedTabs()
verifyEmptyRecentlyClosedTabsList()
}
}
@ -893,7 +821,10 @@ class SmokeTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun createFirstCollectionTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -924,7 +855,10 @@ class SmokeTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun verifyExpandedCollectionItemsTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -977,6 +911,9 @@ class SmokeTest {
@Test
fun shareCollectionTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1000,6 +937,8 @@ class SmokeTest {
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds
fun deleteCollectionTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1013,10 +952,10 @@ class SmokeTest {
}.expandCollection(collectionName) {
clickCollectionThreeDotButton()
selectDeleteCollection()
confirmDeleteCollection()
}
homeScreen {
verifySnackBarText("Collection deleted")
verifyNoCollectionsText()
}
}
@ -1049,7 +988,7 @@ class SmokeTest {
verifyFolderTitle("My Folder")
}.openThreeDotMenu("My Folder") {
}.clickDelete {
confirmFolderDeletion()
confirmDeletion()
verifyDeleteSnackBarText()
navigateUp()
}
@ -1163,7 +1102,7 @@ class SmokeTest {
@Test
@Ignore("To be re-enabled later. See https://github.com/mozilla-mobile/fenix/issues/20716")
fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/"
val pwaPage = "https://mozilla-mobile.github.io/testapp/"
navigationToolbar {
}.enterURLAndEnterToBrowser(pwaPage.toUri()) {
@ -1418,6 +1357,8 @@ class SmokeTest {
@Test
fun goToHomeScreenBottomToolbarTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1430,6 +1371,9 @@ class SmokeTest {
@Test
fun goToHomeScreenTopToolbarTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
@ -1484,17 +1428,21 @@ class SmokeTest {
}
@Test
fun startOnHomeSettingsMenuItemsTest() {
fun tabsSettingsMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
verifyTabViewOptions()
verifyCloseTabsOptions()
verifyStartOnHomeOptions()
}
}
@Test
fun alwaysStartOnHomeTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1506,7 +1454,7 @@ class SmokeTest {
clickAlwaysStartOnHomeToggle()
}
restartApp(activityTestRule)
restartApp(activityTestRule.activityRule)
homeScreen {
verifyHomeScreen()

@ -4,7 +4,6 @@
package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -14,7 +13,6 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -46,13 +44,9 @@ class StrictEnhancedTrackingProtectionTest {
start()
}
activityTestRule.activity.settings().setStrictETP()
// Reset on-boarding notification for each test
TestHelper.setPreference(
InstrumentationRegistry.getInstrumentation().context,
"pref_key_tracking_protection_onboarding", 0
)
val settings = activityTestRule.activity.settings()
settings.setStrictETP()
settings.shouldShowJumpBackInCFR = false
}
@After
@ -76,99 +70,37 @@ class StrictEnhancedTrackingProtectionTest {
}
}
@Test
fun testStrictVisitContentNotification() {
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
}
@Test
fun testStrictVisitContentShield() {
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionShield()
}
}
@Test
fun testStrictVisitProtectionSheet() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionShield()
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}
}
@Test
fun testStrictVisitDisable() {
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionShield()
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.disableEnhancedTrackingProtectionFromSheet {
verifyEnhancedTrackingProtectionSheetStatus("OFF", false)
}.closeEnhancedTrackingProtectionSheet {}
// Verify that Enhanced Tracking Protection remains globally enabled
navigationToolbar {
}.openThreeDotMenu {
verifyThreeDotMenuExists()
}.openSettings {
verifyEnhancedTrackingProtectionButton()
verifyEnhancedTrackingProtectionValue("On")
}
}
@Test
fun testStrictVisitDisableExceptionToggle() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionShield()
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.disableEnhancedTrackingProtectionFromSheet {
@ -189,22 +121,26 @@ class StrictEnhancedTrackingProtectionTest {
@Test
fun testStrictVisitSheetDetails() {
val genericPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.openTrackingProtectionTestPage(trackingProtectionTest.url, true) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionShield()
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
}.openDetails {
verifyEnhancedTrackingProtectionDetailsStatus("Blocked")
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
verifyFingerprintersBlocked()
verifyTrackingContentBlocked()
viewTrackingContentBlockList()
}
}
}

@ -12,6 +12,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -47,6 +48,7 @@ class TabbedBrowsingTest {
@Before
fun setUp() {
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -281,7 +283,6 @@ class TabbedBrowsingTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
// verifyPageContent(defaultWebPage.content)
}.openTabDrawer {
verifyExistingTabList()
verifyNewTabButton()
@ -289,7 +290,9 @@ class TabbedBrowsingTest {
verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title)
}.openNewTab {
}.dismissSearchBar { }
verifySearchBarEmpty()
verifyKeyboardVisibility()
}
}
@Test
@ -298,14 +301,6 @@ class TabbedBrowsingTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
// verifyPageContent(defaultWebPage.content)
}.openTabDrawer {
verifyExistingTabList()
verifyNewTabButton()
verifyTabTrayOverflowMenu(true)
verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title)
}.closeTabDrawer {
}.openTabButtonShortcutsMenu {
verifyTabButtonShortcutMenuItems()
}.closeTabFromShortcutsMenu {
@ -316,28 +311,19 @@ class TabbedBrowsingTest {
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
verifyTabButton()
verifyPrivateSessionMessage()
verifyHomeToolbar()
verifyHomeComponent()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openTabButtonShortcutsMenu {
}.openTabFromShortcutsMenu {
verifyKeyboardVisible()
verifyFocusedNavigationToolbar()
// dismiss search dialog
homeScreen { }.pressBack()
verifyHomeMenu()
verifyHomeWordmark()
verifyTabButton()
verifyHomeToolbar()
verifyHomeComponent()
}
}
}

@ -9,6 +9,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen
@ -28,6 +29,7 @@ class ThreeDotMenuMainTest {
@Before
fun setUp() {
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()

@ -11,6 +11,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
@ -48,6 +49,8 @@ class TopSitesTest {
@Test
fun verifyAddToFirefoxHome() {
val settings = activityIntentTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
@ -58,9 +61,7 @@ class TopSitesTest {
verifyAddToTopSitesButton()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}
@ -68,6 +69,8 @@ class TopSitesTest {
@Test
fun verifyOpenTopSiteNormalTab() {
val settings = activityIntentTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
@ -78,16 +81,12 @@ class TopSitesTest {
verifyAddToTopSitesButton()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openTopSiteTabWithTitle(title = defaultWebPageTitle) {
verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -100,6 +99,8 @@ class TopSitesTest {
@Test
fun verifyOpenTopSitePrivateTab() {
val settings = activityIntentTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
@ -110,9 +111,7 @@ class TopSitesTest {
verifyAddToTopSitesButton()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -124,6 +123,8 @@ class TopSitesTest {
@Test
fun verifyRenameTopSite() {
val settings = activityIntentTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
val defaultWebPageTitleNew = "Test_Page_2"
@ -135,9 +136,7 @@ class TopSitesTest {
verifyAddToTopSitesButton()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -150,6 +149,8 @@ class TopSitesTest {
@Test
fun verifyRemoveTopSite() {
val settings = activityIntentTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
@ -160,9 +161,7 @@ class TopSitesTest {
verifyAddToTopSitesButton()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
}.goToHomescreen {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {

@ -52,6 +52,10 @@ class BookmarksRobot {
assertBookmarksView()
}
fun verifyAddFolderButton() = assertAddFolderButton()
fun verifyCloseButton() = assertCloseButton()
fun verifyDeleteMultipleBookmarksSnackBar() = assertSnackBarText("Bookmarks deleted")
fun verifyBookmarkFavicon(forUrl: Uri) = assertBookmarkFavicon(forUrl)
@ -197,13 +201,15 @@ class BookmarksRobot {
fun longTapDesktopFolder(title: String) = onView(withText(title)).perform(longClick())
fun confirmFolderDeletion() {
fun confirmDeletion() {
onView(withText(R.string.delete_browsing_data_prompt_allow))
.inRoot(RootMatchers.isDialog())
.check(matches(isDisplayed()))
.click()
}
fun clickDeleteInEditModeButton() = deleteInEditModeButton().click()
class Transition {
fun closeMenu(interact: HomeScreenRobot.() -> Unit): Transition {
closeButton().click()
@ -290,6 +296,8 @@ private fun bookmarkURLEditBox() = onView(withId(R.id.bookmarkUrlEdit))
private fun saveBookmarkButton() = onView(withId(R.id.save_bookmark_button))
private fun deleteInEditModeButton() = onView(withId(R.id.delete_bookmark_button))
private fun signInToSyncButton() = onView(withId(R.id.bookmark_folders_sign_in))
private fun assertBookmarksView() {
@ -302,6 +310,11 @@ private fun assertBookmarksView() {
.check(matches(isDisplayed()))
}
private fun assertAddFolderButton() =
addFolderButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertCloseButton() = closeButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertEmptyBookmarksList() =
onView(withId(R.id.bookmarks_empty_view)).check(matches(withText("No bookmarks here")))

@ -26,7 +26,6 @@ import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
@ -39,7 +38,6 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.mediasession.MediaSession
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matchers.not
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.mozilla.fenix.R
@ -164,15 +162,9 @@ class BrowserRobot {
fun verifyEnhancedTrackingProtectionSwitch() = assertEnhancedTrackingProtectionSwitch()
fun clickEnhancedTrackingProtectionSwitchOffOn() =
onView(withResourceName("switch_widget")).click()
fun verifyProtectionSettingsButton() = assertProtectionSettingsButton()
fun verifyEnhancedTrackingOptions() {
clickEnhancedTrackingProtectionPanel()
onView(withId(R.id.mozac_browser_toolbar_security_indicator)).click()
verifyEnhancedTrackingProtectionSwitch()
verifyProtectionSettingsButton()
}
fun verifyMenuButton() = assertMenuButton()
@ -214,11 +206,6 @@ class BrowserRobot {
.perform(ViewActions.pressBack())
}
fun clickEnhancedTrackingProtectionPanel() = enhancedTrackingProtectionIndicator().click()
fun verifyEnhancedTrackingProtectionPanelNotVisible() =
assertEnhancedTrackingProtectionIndicatorNotVisible()
fun clickContextOpenLinkInNewTab() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
@ -419,7 +406,9 @@ class BrowserRobot {
.className(EditText::class.java)
)
passwordField.waitForExists(waitingTime)
passwordField.setText(password)
passwordField.click()
passwordField.clearTextField()
passwordField.text = password
// wait until the password is hidden
assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime))
}
@ -461,10 +450,10 @@ class BrowserRobot {
fun swipeNavBarRight(tabUrl: String) {
// failing to swipe on Firebase sometimes, so it tries again
try {
navURLBar().perform(ViewActions.swipeRight())
navURLBar().swipeRight(2)
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
} catch (e: AssertionError) {
navURLBar().perform(ViewActions.swipeRight())
navURLBar().swipeRight(2)
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
}
}
@ -472,10 +461,10 @@ class BrowserRobot {
fun swipeNavBarLeft(tabUrl: String) {
// failing to swipe on Firebase sometimes, so it tries again
try {
navURLBar().perform(ViewActions.swipeLeft())
navURLBar().swipeLeft(2)
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
} catch (e: AssertionError) {
navURLBar().perform(ViewActions.swipeLeft())
navURLBar().swipeLeft(2)
assertTrue(mDevice.findObject(UiSelector().text(tabUrl)).waitUntilGone(waitingTime))
}
}
@ -499,7 +488,8 @@ class BrowserRobot {
}
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.waitForIdle(waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navURLBar().click()
NavigationToolbarRobot().interact()
@ -507,23 +497,17 @@ class BrowserRobot {
}
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitForIdle(waitingTime)
mDevice.waitNotNull(Until.findObject(By.desc("Tabs")))
tabsCounter().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime
)
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")))
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
}
fun openTabButtonShortcutsMenu(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.waitForIdle(waitingTime)
tabsCounter().perform(
ViewActions.longClick()
)
mDevice.waitNotNull(Until.findObject(By.desc("Tabs")))
tabsCounter().click(LONG_CLICK_DURATION)
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -540,6 +524,14 @@ class BrowserRobot {
onView(withContentDescription("Home screen"))
.check(matches(isDisplayed()))
.click()
mDevice.waitForIdle()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun goBack(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.pressBack()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
@ -566,27 +558,11 @@ fun browserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
return BrowserRobot.Transition()
}
private fun dismissOnboardingButton() = onView(withId(R.id.close_onboarding))
fun navURLBar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
fun dismissTrackingOnboarding() {
mDevice.wait(Until.findObject(By.res("close_onboarding")), waitingTime)
dismissOnboardingButton().click()
}
private fun assertNavURLBar() = assertTrue(navURLBar().waitForExists(waitingTime))
fun navURLBar() = onView(withId(R.id.toolbar))
private fun assertNavURLBar() = navURLBar()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNavURLBarHidden() = navURLBar()
.check(matches(not(isDisplayed())))
fun enhancedTrackingProtectionIndicator() =
onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator))
private fun assertEnhancedTrackingProtectionIndicatorNotVisible() {
enhancedTrackingProtectionIndicator().check(matches(not(isDisplayed())))
}
private fun assertNavURLBarHidden() = assertTrue(navURLBar().waitUntilGone(waitingTime))
private fun assertEnhancedTrackingProtectionSwitch() {
withText(R.id.trackingProtectionSwitch)
@ -610,7 +586,7 @@ private fun assertMenuButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun tabsCounter() = onView(withId(R.id.counter_box))
private fun tabsCounter() = mDevice.findObject(By.desc("Tabs"))
private fun mediaPlayerPlayButton() =
mDevice.findObject(

@ -6,6 +6,7 @@ import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
@ -131,7 +132,6 @@ class CollectionRobot {
fun selectDeleteCollection() {
onView(withText("Delete collection")).click()
mDevice.waitNotNull(Until.findObject(By.res("android:id/message")), waitingTime)
}
fun confirmDeleteCollection() {
@ -192,6 +192,8 @@ class CollectionRobot {
fun goBackInCollectionFlow() = backButton().click()
fun swipeToBottom() = onView(withId(R.id.sessionControlRecyclerView)).perform(swipeUp())
class Transition {
fun collapseCollection(
title: String,

@ -28,6 +28,7 @@ import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -42,7 +43,7 @@ class DownloadRobot {
fun verifyDownloadNotificationPopup() = assertDownloadNotificationPopup()
fun verifyPhotosAppOpens() = assertPhotosOpens()
fun verifyPhotosAppOpens() = assertExternalAppOpens(GOOGLE_APPS_PHOTOS)
fun verifyDownloadedFileName(fileName: String) {
mDevice.findObject(UiSelector().text(fileName)).waitForExists(waitingTime)
@ -144,18 +145,6 @@ private fun clickOpenButton() =
matches(isDisplayed())
)
private fun assertPhotosOpens() {
if (isPackageInstalled(GOOGLE_APPS_PHOTOS)) {
Intents.intended(IntentMatchers.toPackage(GOOGLE_APPS_PHOTOS))
} else {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
Until.findObject(By.text("Could not open file")),
TestAssetHelper.waitingTime
)
}
}
private fun downloadedFile(fileName: String) = onView(withText(fileName))
private fun assertDownloadedFileIcon() = onView(withId(R.id.favicon)).check(matches(isDisplayed()))

@ -7,7 +7,6 @@
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
@ -18,9 +17,9 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.TestCase.assertTrue
import org.hamcrest.Matchers.containsString
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
@ -34,10 +33,6 @@ class EnhancedTrackingProtectionRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
fun verifyEnhancedTrackingProtectionNotice() = assertEnhancedTrackingProtectionNotice()
fun verifyEnhancedTrackingProtectionShield() = assertEnhancedTrackingProtectionShield()
fun verifyEnhancedTrackingProtectionSheetStatus(status: String, state: Boolean) =
assertEnhancedTrackingProtectionSheetStatus(status, state)
@ -50,29 +45,41 @@ class EnhancedTrackingProtectionRobot {
fun verifyCryptominersBlocked() = assertCryptominersBlocked()
fun verifyBasicLevelTrackingContentBlocked() = assertBasicLevelTrackingContentBlocked()
fun verifyTrackingContentBlocked() = assertTrackingContentBlocked()
fun viewTrackingContentBlockList() {
trackingContentBlockListButton()
.check(matches(isDisplayed()))
.click()
onView(withId(R.id.blocking_text_list))
.check(
matches(
withText(
containsString(
"social-track-digest256.dummytracker.org\n" +
"ads-track-digest256.dummytracker.org\n" +
"analytics-track-digest256.dummytracker.org"
)
)
)
)
}
class Transition {
fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionSheet().waitForExists(waitingTime)
openEnhancedTrackingProtectionSheet().click()
EnhancedTrackingProtectionRobot().interact()
return Transition()
}
fun closeNotificationPopup(interact: BrowserRobot.() -> Unit): Transition {
closeButton().click()
BrowserRobot().interact()
return Transition()
}
fun closeEnhancedTrackingProtectionSheet(interact: BrowserRobot.() -> Unit): Transition {
fun closeEnhancedTrackingProtectionSheet(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
// Back out of the Enhanced Tracking Protection sheet
mDevice.pressBack()
BrowserRobot().interact()
return Transition()
return BrowserRobot.Transition()
}
fun disableEnhancedTrackingProtectionFromSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
@ -83,13 +90,16 @@ class EnhancedTrackingProtectionRobot {
}
fun openProtectionSettings(interact: SettingsSubMenuEnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionSettings().click()
openEnhancedTrackingProtectionDetails().waitForExists(waitingTime)
openEnhancedTrackingProtectionDetails().click()
trackingProtectionSettingsButton().click()
SettingsSubMenuEnhancedTrackingProtectionRobot().interact()
return Transition()
}
fun openDetails(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionDetails().waitForExists(waitingTime)
openEnhancedTrackingProtectionDetails().click()
EnhancedTrackingProtectionRobot().interact()
@ -103,23 +113,10 @@ fun enhancedTrackingProtection(interact: EnhancedTrackingProtectionRobot.() -> U
return EnhancedTrackingProtectionRobot.Transition()
}
private fun assertEnhancedTrackingProtectionNotice() {
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/onboarding_message")),
TestAssetHelper.waitingTime
)
}
private fun assertEnhancedTrackingProtectionShield() {
mDevice.waitNotNull(
Until.findObjects(By.descContains("Tracking Protection has blocked trackers"))
)
}
private fun assertEnhancedTrackingProtectionSheetStatus(status: String, state: Boolean) {
mDevice.waitNotNull(Until.findObjects(By.textContains(status)))
onView(ViewMatchers.withResourceName("switch_widget")).check(
ViewAssertions.matches(
matches(
isChecked(
state
)
@ -131,25 +128,23 @@ private fun assertEnhancedTrackingProtectionDetailsStatus(status: String) {
mDevice.waitNotNull(Until.findObjects(By.textContains(status)))
}
private fun closeButton() = onView(ViewMatchers.withId(R.id.close_onboarding))
private fun openEnhancedTrackingProtectionSheet() =
onView(ViewMatchers.withId(R.id.mozac_browser_toolbar_tracking_protection_indicator))
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_security_indicator"))
private fun disableEnhancedTrackingProtection() =
onView(ViewMatchers.withResourceName("switch_widget"))
private fun openEnhancedTrackingProtectionSettings() =
onView(ViewMatchers.withId(R.id.protection_settings))
private fun trackingProtectionSettingsButton() =
onView(withId(R.id.protection_settings))
private fun openEnhancedTrackingProtectionDetails() =
onView(ViewMatchers.withId(R.id.tracking_content))
mDevice.findObject(UiSelector().resourceId("$packageName:id/trackingProtectionDetails"))
private fun assertTrackingCookiesBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.cross_site_tracking)).check(matches(isDisplayed()))
onView(withId(R.id.tracking_content)).check(matches(isDisplayed()))
}
private fun assertFingerprintersBlocked() {
@ -166,23 +161,11 @@ private fun assertCryptominersBlocked() {
onView(withId(R.id.cryptominers)).check(matches(isDisplayed()))
}
private fun assertBasicLevelTrackingContentBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/tracking_content"))
.waitForExists(waitingTime)
onView(withId(R.id.tracking_content))
.check(matches(isDisplayed()))
.click()
onView(withId(R.id.blocking_text_list))
.check(
matches(
withText(
containsString(
"social-track-digest256.dummytracker.org\n" +
"ads-track-digest256.dummytracker.org\n" +
"analytics-track-digest256.dummytracker.org"
)
)
)
)
private fun assertTrackingContentBlocked() {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/tracking_content"))
.waitForExists(waitingTime)
)
}
private fun trackingContentBlockListButton() = onView(withId(R.id.tracking_content))

@ -19,8 +19,10 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -51,6 +53,8 @@ class HistoryRobot {
assertVisitedTimeTitle()
}
fun verifyHistoryItemExists(url: String) = assertHistoryItemExists(url)
fun verifyFirstTestPageTitle(title: String) = assertTestPageTitle(title)
fun verifyTestPageUrl(expectedUrl: Uri) = assertPageUrl(expectedUrl)
@ -61,21 +65,12 @@ class HistoryRobot {
fun verifyHomeScreen() = HomeScreenRobot().verifyHomeScreen()
fun openOverflowMenu() {
mDevice.waitNotNull(
Until.findObject(
By.res("org.mozilla.fenix.debug:id/overflow_menu")
),
waitingTime
)
threeDotMenu().click()
}
fun clickDeleteHistoryButton() {
mDevice.waitNotNull(Until.findObject(By.text("Delete history")), waitingTime)
deleteAllHistoryButton().click()
deleteButton().click()
}
fun clickDeleteAllHistoryButton() = deleteAllButton().click()
fun confirmDeleteAllHistory() {
onView(withText("Delete"))
.inRoot(isDialog())
@ -92,15 +87,6 @@ class HistoryRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openThreeDotMenu(interact: ThreeDotMenuHistoryItemRobot.() -> Unit):
ThreeDotMenuHistoryItemRobot.Transition {
threeDotMenu().click()
ThreeDotMenuHistoryItemRobot().interact()
return ThreeDotMenuHistoryItemRobot.Transition()
}
}
}
@ -113,11 +99,11 @@ private fun testPageTitle() = onView(allOf(withId(R.id.title), withText("Test_Pa
private fun pageUrl() = onView(withId(R.id.url))
private fun threeDotMenu() = onView(withId(R.id.overflow_menu))
private fun deleteButton() = onView(withId(R.id.overflow_menu))
private fun snackBarText() = onView(withId(R.id.snackbar_text))
private fun deleteAllButton() = onView(withId(R.id.history_delete_all))
private fun deleteAllHistoryButton() = onView(withId(R.id.delete_button))
private fun snackBarText() = onView(withId(R.id.snackbar_text))
private fun assertHistoryMenuView() {
onView(
@ -138,6 +124,11 @@ private fun assertEmptyHistoryView() =
private fun assertHistoryListExists() =
mDevice.findObject(UiSelector().resourceId("R.id.history_list")).waitForExists(waitingTime)
private fun assertHistoryItemExists(url: String) {
mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains(url)))
assertTrue(mDevice.findObject(UiSelector().textContains(url)).waitForExists(waitingTime))
}
private fun assertVisitedTimeTitle() =
onView(withId(R.id.header_title)).check(matches(withText("Today")))

@ -170,8 +170,17 @@ class HomeScreenRobot {
}
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/menuButton")), waitingTime)
threeDotButton().perform(click())
// Issue: https://github.com/mozilla-mobile/fenix/issues/21578
try {
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/menuButton")),
waitingTime
)
} catch (e: AssertionError) {
mDevice.pressBack()
} finally {
threeDotButton().perform(click())
}
ThreeDotMenuMainRobot().interact()
return ThreeDotMenuMainRobot.Transition()
@ -180,7 +189,7 @@ class HomeScreenRobot {
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar().perform(click())
navigationToolbar().click()
SearchRobot().interact()
return SearchRobot.Transition()
@ -229,7 +238,7 @@ class HomeScreenRobot {
fun openNavigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar().perform(click())
navigationToolbar().click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
@ -344,10 +353,9 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
.contains("mInputShown=true")
)
private fun navigationToolbar() = onView(withId(R.id.toolbar))
private fun navigationToolbar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
private fun assertNavigationToolbar() =
navigationToolbar().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNavigationToolbar() = assertTrue(navigationToolbar().waitForExists(waitingTime))
private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address")))

@ -12,15 +12,11 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -32,17 +28,14 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreEqualTo
import org.mozilla.fenix.helpers.assertions.AwesomeBarAssertion.Companion.suggestionsAreGreaterThan
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -51,12 +44,6 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/
class NavigationToolbarRobot {
fun verifySearchSuggestionsAreMoreThan(suggestionSize: Int) =
assertSuggestionsAreMoreThan(suggestionSize)
fun verifySearchSuggestionsAreEqualTo(suggestionSize: Int) =
assertSuggestionsAreEqualTo(suggestionSize)
fun verifyNoHistoryBookmarks() = assertNoHistoryBookmarks()
fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems()
@ -67,7 +54,7 @@ class NavigationToolbarRobot {
fun verifyCloseReaderViewDetected(visible: Boolean = false) =
assertCloseReaderViewDetected(visible)
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
fun typeSearchTerm(searchTerm: String) = awesomeBar().setText(searchTerm)
fun toggleReaderView() {
mDevice.findObject(
@ -87,7 +74,14 @@ class NavigationToolbarRobot {
fun goBackToWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
openEditURLView()
clearAddressBar().click()
awesomeBar().check((matches(withText(containsString("")))))
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.textContains("")
).waitForExists(waitingTime)
)
goBackButton()
BrowserRobot().interact()
@ -102,13 +96,13 @@ class NavigationToolbarRobot {
openEditURLView()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
awesomeBar().setText(url.toString())
mDevice.pressEnter()
runWithIdleRes(sessionLoadedIdlingResource) {
onView(
anyOf(
withResourceName("browserLayout"),
withResourceName("onboarding_message"), // Req ETP dialog
withResourceName("download_button")
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
@ -118,47 +112,6 @@ class NavigationToolbarRobot {
return BrowserRobot.Transition()
}
fun openTrackingProtectionTestPage(
url: Uri,
etpEnabled: Boolean,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
openEditURLView()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
val onboardingMessage =
mDevice.findObject(UiSelector().resourceId("$packageName:id/onboarding_message"))
val onboardingDisplayed = onboardingMessage.waitForExists(waitingTime)
when (etpEnabled) {
true ->
try {
assertTrue(
"Onboarding message not displayed",
onboardingDisplayed
)
} catch (e: AssertionError) {
openThreeDotMenu {
}.stopPageLoad {
if (!onboardingDisplayed) {
openThreeDotMenu {
}.refreshPage {
assertTrue(onboardingDisplayed)
}
}
}
}
false ->
onView(withResourceName("browserLayout")).check(matches(isDisplayed()))
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openTabCrashReporter(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
val crashUrl = "about:crashcontent"
@ -166,7 +119,8 @@ class NavigationToolbarRobot {
openEditURLView()
awesomeBar().perform(replaceText(crashUrl), pressImeActionButton())
awesomeBar().setText(crashUrl)
mDevice.pressEnter()
runWithIdleRes(sessionLoadedIdlingResource) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/crash_tab_image"))
@ -203,15 +157,11 @@ class NavigationToolbarRobot {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")), waitingTime)
urlBar().click()
awesomeBar().perform(replaceText(url.toString()), pressImeActionButton())
awesomeBar().setText(url.toString())
mDevice.pressEnter()
runWithIdleRes(sessionLoadedIdlingResource) {
onView(
anyOf(
ViewMatchers.withResourceName("browserLayout"),
ViewMatchers.withResourceName("onboarding_message") // Req for ETP dialog
)
)
onView(ViewMatchers.withResourceName("browserLayout"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
@ -298,6 +248,17 @@ class NavigationToolbarRobot {
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
urlBar().click()
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
).waitForExists(waitingTime)
SearchRobot().interact()
return SearchRobot.Transition()
}
}
}
@ -306,12 +267,6 @@ fun navigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationTo
return NavigationToolbarRobot.Transition()
}
fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
urlBar().click()
SearchRobot().interact()
return SearchRobot.Transition()
}
fun openEditURLView() {
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/toolbar")),
@ -324,16 +279,6 @@ fun openEditURLView() {
)
}
private fun assertSuggestionsAreEqualTo(suggestionSize: Int) {
mDevice.waitForIdle()
onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize))
}
private fun assertSuggestionsAreMoreThan(suggestionSize: Int) {
mDevice.waitForIdle()
onView(withId(R.id.awesome_bar)).check(suggestionsAreGreaterThan(suggestionSize))
}
private fun assertNoHistoryBookmarks() {
onView(withId(R.id.container))
.check(matches(not(hasDescendant(withText("Test_Page_1")))))
@ -348,13 +293,14 @@ private fun assertTabButtonShortcutMenuItems() {
.check(matches(hasDescendant(withText("New tab"))))
}
private fun dismissOnboardingButton() = onView(withId(R.id.close_onboarding))
private fun urlBar() = onView(withId(R.id.toolbar))
private fun awesomeBar() = onView(withId(R.id.mozac_browser_toolbar_edit_url_view))
private fun urlBar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
private fun awesomeBar() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu))
private fun tabTrayButton() = onView(withId(R.id.tab_button))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBar() = onView(withId(R.id.mozac_browser_toolbar_clear_view))
private fun clearAddressBar() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"))
private fun goBackButton() = mDevice.pressBack()
private fun readerViewToggle() =
onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))

@ -41,44 +41,11 @@ class RecentlyClosedTabsRobot {
fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) = assertPageUrl(expectedUrl)
fun openRecentlyClosedTabsThreeDotMenu() = recentlyClosedTabsThreeDotButton().click()
fun verifyRecentlyClosedTabsMenuCopy() = assertRecentlyClosedTabsMenuCopy()
fun verifyRecentlyClosedTabsMenuShare() = assertRecentlyClosedTabsMenuShare()
fun verifyRecentlyClosedTabsMenuNewTab() = assertRecentlyClosedTabsOverlayNewTab()
fun verifyRecentlyClosedTabsMenuPrivateTab() = assertRecentlyClosedTabsMenuPrivateTab()
fun verifyRecentlyClosedTabsMenuDelete() = assertRecentlyClosedTabsMenuDelete()
fun clickCopyRecentlyClosedTabs() = recentlyClosedTabsCopyButton().click()
fun clickShareRecentlyClosedTabs() = recentlyClosedTabsShareButton().click()
fun clickDeleteCopyRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click()
fun verifyCopyRecentlyClosedTabsSnackBarText() = assertCopySnackBarText()
fun verifyShareOverlay() = assertRecentlyClosedShareOverlay()
fun verifyShareTabFavicon() = assertRecentlyClosedShareFavicon()
fun verifyShareTabTitle(title: String) = assetRecentlyClosedShareTitle(title)
fun verifyShareTabUrl(expectedUrl: Uri) = assertRecentlyClosedShareUrl(expectedUrl)
fun clickDeleteRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click()
class Transition {
fun clickOpenInNewTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
recentlyClosedTabsNewTabButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickOpenInPrivateTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
recentlyClosedTabsNewPrivateTabButton().click()
recentlyClosedTabsPageTitle().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -138,7 +105,7 @@ private fun assertRecentlyClosedTabsPageTitle(title: String) {
)
}
private fun recentlyClosedTabsThreeDotButton() =
private fun recentlyClosedTabsDeleteButton() =
onView(
allOf(
withId(R.id.overflow_menu),
@ -147,93 +114,3 @@ private fun recentlyClosedTabsThreeDotButton() =
)
)
)
private fun assertRecentlyClosedTabsMenuCopy() =
onView(withText("Copy"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuShare() =
onView(withText("Share"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsOverlayNewTab() =
onView(withText("Open in new tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuPrivateTab() =
onView(withText("Open in private tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuDelete() =
onView(withText("Delete"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun recentlyClosedTabsCopyButton() = onView(withText("Copy"))
private fun copySnackBarText() = onView(withId(R.id.snackbar_text))
private fun assertCopySnackBarText() = copySnackBarText()
.check(
matches
(withText("URL copied"))
)
private fun recentlyClosedTabsShareButton() = onView(withText("Share"))
private fun assertRecentlyClosedShareOverlay() =
onView(withId(R.id.shareWrapper))
.check(
matches(ViewMatchers.isDisplayed())
)
private fun assetRecentlyClosedShareTitle(title: String) =
onView(withId(R.id.share_tab_title))
.check(
matches(ViewMatchers.isDisplayed())
)
.check(
matches(withText(title))
)
private fun assertRecentlyClosedShareFavicon() =
onView(withId(R.id.share_tab_favicon))
.check(
matches(ViewMatchers.isDisplayed())
)
private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
onView(
allOf(
withId(R.id.share_tab_url),
withEffectiveVisibility(Visibility.VISIBLE)
)
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString())))
)
private fun recentlyClosedTabsNewTabButton() = onView(withText("Open in new tab"))
private fun recentlyClosedTabsNewPrivateTabButton() = onView(withText("Open in private tab"))
private fun recentlyClosedTabsDeleteButton() = onView(withText("Delete"))

@ -6,19 +6,23 @@
package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView
import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertHasClickAction
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollToIndex
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -31,16 +35,17 @@ import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.startsWith
import org.hamcrest.Matchers
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -53,24 +58,28 @@ class SearchRobot {
fun verifyScanButton() = assertScanButton()
fun verifySearchEngineButton() = assertSearchEngineButton()
fun verifySearchWithText() = assertSearchWithText()
fun verifySearchEngineResults(searchEngineName: String) =
assertSearchEngineResults(searchEngineName)
fun verifySearchEngineResults(rule: ComposeTestRule, searchEngineName: String, count: Int) =
assertSearchEngineResults(rule, searchEngineName, count)
fun verifySearchEngineSuggestionResults(rule: ComposeTestRule, searchSuggestion: String) =
assertSearchEngineSuggestionResults(rule, searchSuggestion)
fun verifyNoSuggestionsAreDisplayed(rule: ComposeTestRule, searchSuggestion: String) =
assertNoSuggestionsAreDisplayed(rule, searchSuggestion)
fun verifySearchEngineURL(searchEngineName: String) = assertSearchEngineURL(searchEngineName)
fun verifySearchSettings() = assertSearchSettings()
fun verifySearchBarEmpty() = assertSearchBarEmpty()
fun verifyKeyboardVisibility() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifySearchEngineList() = assertSearchEngineList()
fun verifySearchEngineList(rule: ComposeTestRule) = rule.assertSearchEngineList()
fun verifySearchEngineIcon(expectedText: String) {
onView(withContentDescription(expectedText))
}
fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText)
fun verifyEnginesListShortcutContains(searchEngineName: String) = assertEngineListShortcutContains(searchEngineName)
fun verifyEnginesListShortcutContains(rule: ComposeTestRule, searchEngineName: String) = assertEngineListShortcutContains(rule, searchEngineName)
fun changeDefaultSearchEngine(searchEngineName: String) =
selectDefaultSearchEngine(searchEngineName)
fun changeDefaultSearchEngine(rule: ComposeTestRule, searchEngineName: String) =
rule.selectDefaultSearchEngine(searchEngineName)
fun clickSearchEngineShortcutButton() {
val searchEnginesShortcutButton = mDevice.findObject(
@ -94,40 +103,61 @@ class SearchRobot {
}
fun typeSearch(searchTerm: String) {
browserToolbarEditView().perform(typeText(searchTerm))
browserToolbarEditView().setText(searchTerm)
mDevice.waitForIdle()
}
fun clickSearchEngineButton(searchEngineName: String) {
searchEngineButton(searchEngineName).perform(click())
fun clickSearchEngineButton(rule: ComposeTestRule, searchEngineName: String) {
rule.waitForIdle()
mDevice.waitForObjects(
mDevice.findObject(
UiSelector().textContains(searchEngineName)
)
)
rule.onNodeWithText(searchEngineName)
.assertExists()
.assertHasClickAction()
.performClick()
}
fun clickSearchEngineResult(searchEngineName: String) {
fun clickSearchEngineResult(rule: ComposeTestRule, searchEngineName: String) {
mDevice.waitNotNull(
Until.findObjects(By.text(searchEngineName)),
TestAssetHelper.waitingTime
)
awesomeBar().perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
0,
click()
)
)
rule.onAllNodesWithText(searchEngineName)
.onFirst()
.assertIsDisplayed()
.assertHasClickAction()
.performClick()
}
fun scrollToSearchEngineSettings() {
@OptIn(ExperimentalTestApi::class)
fun scrollToSearchEngineSettings(rule: ComposeTestRule) {
// Soft keyboard is visible on screen on view access; hide it
onView(allOf(withId(R.id.search_wrapper))).perform(
closeSoftKeyboard()
)
onView(allOf(withId(R.id.awesome_bar))).perform(ViewActions.swipeUp())
mDevice.findObject(UiSelector().text("Google"))
.waitForExists(waitingTime)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
}
fun clickSearchEngineSettings() {
onView(withText("Search engine settings")).perform(click())
fun clickSearchEngineSettings(rule: ComposeTestRule) {
rule.onNodeWithText("Search engine settings")
.assertIsDisplayed()
.assertHasClickAction()
.performClick()
}
fun clickClearButton() {
clearButton().perform(click())
clearButton().click()
}
fun longClickToolbar() {
@ -154,14 +184,23 @@ class SearchRobot {
fun dismissSearchBar(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.waitForIdle()
closeSoftKeyboard()
mDevice.pressBack()
try {
assertTrue(searchWrapper().waitUntilGone(waitingTimeShort))
} catch (e: AssertionError) {
mDevice.pressBack()
assertTrue(searchWrapper().waitUntilGone(waitingTimeShort))
}
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle()
browserToolbarEditView().perform(typeText("mozilla\n"))
browserToolbarEditView().setText("mozilla\n")
mDevice.pressEnter()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -170,16 +209,15 @@ class SearchRobot {
fun submitQuery(query: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitForIdle()
browserToolbarEditView().perform(typeText(query + "\n"))
browserToolbarEditView().setText(query)
mDevice.pressEnter()
runWithIdleRes(sessionLoadedIdlingResource) {
onView(
anyOf(
ViewMatchers.withResourceName("browserLayout"),
ViewMatchers.withResourceName("onboarding_message") // Req ETP dialog
)
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/browserLayout")
).waitForExists(waitingTime)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
BrowserRobot().interact()
@ -193,15 +231,8 @@ class SearchRobot {
}
}
private fun awesomeBar() = onView(withId(R.id.awesome_bar))
private fun browserToolbarEditView() =
onView(Matchers.allOf(withId(R.id.mozac_browser_toolbar_edit_url_view)))
private fun searchEngineButton(searchEngineName: String): ViewInteraction {
mDevice.waitNotNull(Until.findObject(By.text(searchEngineName)), TestAssetHelper.waitingTime)
return onView(Matchers.allOf(withText(searchEngineName)))
}
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"))
private fun denyPermissionButton(): UiObject {
mDevice.waitNotNull(Until.findObjects(By.text("Deny")), TestAssetHelper.waitingTime)
@ -215,12 +246,13 @@ private fun allowPermissionButton(): UiObject {
private fun scanButton(): ViewInteraction {
mDevice.waitNotNull(Until.findObject(By.res("org.mozilla.fenix.debug:id/search_scan_button")), TestAssetHelper.waitingTime)
return onView(allOf(withId(R.id.search_scan_button)))
return onView(allOf(withId(R.id.qr_scan_button)))
}
private fun clearButton() = onView(withId(R.id.mozac_browser_toolbar_clear_view))
private fun clearButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"))
private fun searchWrapper() = onView(withId(R.id.search_wrapper))
private fun searchWrapper() = mDevice.findObject(UiSelector().resourceId("$packageName:id/search_wrapper"))
private fun assertSearchEngineURL(searchEngineName: String) {
mDevice.waitNotNull(
@ -231,27 +263,66 @@ private fun assertSearchEngineURL(searchEngineName: String) {
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertSearchEngineResults(searchEngineName: String) {
val count =
mDevice.wait(Until.findObjects(By.text((searchEngineName))), TestAssetHelper.waitingTime)
assert(count.size > 1)
private fun assertSearchEngineResults(rule: ComposeTestRule, searchEngineName: String, count: Int) {
rule.waitForIdle()
mDevice.waitForObjects(
mDevice.findObject(
UiSelector().textContains(searchEngineName)
)
)
rule.onAllNodesWithText(searchEngineName)
.assertCountEquals(count)
}
private fun assertSearchEngineSuggestionResults(rule: ComposeTestRule, searchResult: String) {
rule.waitForIdle()
mDevice.waitForObjects(
mDevice.findObject(
UiSelector().textContains(searchResult)
)
)
rule.onNodeWithText(searchResult)
.assertExists()
}
private fun assertSearchView() {
onView(withId(R.id.search_wrapper)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNoSuggestionsAreDisplayed(rule: ComposeTestRule, searchTerm: String) {
rule.waitForIdle()
rule.onNodeWithText(searchTerm)
.assertDoesNotExist()
}
private fun assertSearchView() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_wrapper")
).waitForExists(waitingTime)
)
private fun assertBrowserToolbarEditView() =
onView(Matchers.allOf(withId(R.id.mozac_browser_toolbar_edit_url_view)))
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
).waitForExists(waitingTime)
)
private fun assertScanButton() =
onView(allOf(withText("Scan")))
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/qr_scan_button")
).waitForExists(waitingTime)
)
private fun assertSearchEngineButton() =
onView(withId(R.id.search_engines_shortcut_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_engines_shortcut_button")
).waitForExists(waitingTime)
)
private fun assertSearchWithText() =
onView(allOf(withText("THIS TIME, SEARCH WITH:")))
@ -261,7 +332,14 @@ private fun assertSearchSettings() =
onView(allOf(withText("Default search engine")))
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertSearchBarEmpty() = browserToolbarEditView().check(matches(withText("")))
private fun assertSearchBarEmpty() =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.textContains("")
).waitForExists(waitingTime)
)
fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
SearchRobot().interact()
@ -283,41 +361,67 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = {
)
}
private fun assertSearchEngineList() {
private fun ComposeTestRule.assertSearchEngineList() {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onView(withText("Google"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Amazon.com"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Bing"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("DuckDuckGo"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Wikipedia"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onNodeWithText("Google")
.assertExists()
.assertIsDisplayed()
onNodeWithText("Amazon.com")
.assertExists()
.assertIsDisplayed()
onNodeWithText("Bing")
.assertExists()
.assertIsDisplayed()
onNodeWithText("DuckDuckGo")
.assertExists()
.assertIsDisplayed()
onNodeWithText("Wikipedia")
.assertExists()
.assertIsDisplayed()
}
private fun assertEngineListShortcutContains(searchEngineName: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/awesome_bar"))
.waitForExists(waitingTime)
@OptIn(ExperimentalTestApi::class)
private fun assertEngineListShortcutContains(rule: ComposeTestRule, searchEngineName: String) {
rule.waitForIdle()
mDevice.waitForObjects(
mDevice.findObject(
UiSelector().textContains("Google")
)
)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
onView(withId(R.id.awesome_bar))
.perform(swipeDown())
.check(matches(hasDescendant(withText(searchEngineName))))
rule.onNodeWithText(searchEngineName)
.assertExists()
.assertIsDisplayed()
.assertHasClickAction()
}
private fun selectDefaultSearchEngine(searchEngine: String) {
private fun ComposeTestRule.selectDefaultSearchEngine(searchEngine: String) {
onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click()
onView(withText(searchEngine))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.perform(click())
}
private fun assertDefaultSearchEngine(expectedText: String) {
onView(allOf(withId(R.id.mozac_browser_toolbar_edit_icon), withContentDescription(expectedText)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onNodeWithText(searchEngine)
.assertExists()
.assertIsDisplayed()
.performClick()
}
private fun assertDefaultSearchEngine(expectedText: String) =
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_edit_icon")
.descriptionContains(expectedText)
).waitForExists(waitingTime)
)
private fun assertPastedToolbarText(expectedText: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)

@ -6,7 +6,6 @@
package org.mozilla.fenix.ui.robots
import android.content.pm.PackageManager
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewInteraction
@ -34,6 +33,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_PLAY_SERVICES
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.isPackageInstalled
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click
@ -272,6 +272,11 @@ class SettingsRobot {
}
}
fun settingsScreen(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
SettingsRobot().interact()
return SettingsRobot.Transition()
}
private fun assertSettingsView() {
// verify that we are in the correct library view
assertGeneralHeading()
@ -493,15 +498,6 @@ private fun assertGooglePlayRedirect() {
}
}
fun isPackageInstalled(packageName: String): Boolean {
return try {
val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (exception: PackageManager.NameNotFoundException) {
false
}
}
private fun addonsManagerButton() = onView(withText(R.string.preferences_addons))
private fun goBackButton() =

@ -20,10 +20,18 @@ import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withSubstring
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.Calendar
import java.util.Date
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.R
@ -31,12 +39,6 @@ import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.isVisibleForUser
import org.mozilla.fenix.settings.SupportUtils
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
import java.time.temporal.ChronoField
import java.util.Calendar
import java.util.Date
/**
* Implementation of Robot Pattern for the settings search sub menu.
@ -79,14 +81,24 @@ private fun navigateBackToAboutPage(itemToInteract: () -> Unit) {
}
private fun verifyListElements() {
assertAboutToolbar()
assertWhatIsNewInFirefoxPreview()
navigateBackToAboutPage(::assertSupport)
assertCrashes()
navigateBackToAboutPage(::assertPrivacyNotice)
navigateBackToAboutPage(::assertKnowYourRights)
navigateBackToAboutPage(::assertLicensingInformation)
navigateBackToAboutPage(::assertLibrariesUsed)
}
private fun assertAboutToolbar() =
onView(
allOf(
withId(R.id.navigationToolbar),
hasDescendant(withText("About $appName"))
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertVersionNumber() {
val context = InstrumentationRegistry.getInstrumentation().targetContext
@ -130,16 +142,6 @@ private fun assertWhatIsNewInFirefoxPreview() {
onView(withText("Whats new in $appName"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
// Commenting out since the Text to verify in the web site seems to be different now
/*
TestHelper.verifyUrl(
SupportUtils.SumoTopic.WHATS_NEW.topicStr,
"org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)*/
Espresso.pressBack()
}
private fun assertSupport() {
@ -156,8 +158,30 @@ private fun assertSupport() {
"org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)
}
Espresso.pressBack()
private fun assertCrashes() {
browserScreen {
}.openThreeDotMenu {
}.openSettings {
}.openAboutFirefoxPreview {
}
if (!onView(withText("Crashes")).isVisibleForUser()) {
onView(withId(R.id.about_layout)).perform(ViewActions.swipeUp())
}
onView(withText("Crashes"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
onView(withSubstring("No crash reports have been submitted"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
for (i in 1..3) {
Espresso.pressBack()
}
}
private fun assertPrivacyNotice() {
@ -174,8 +198,6 @@ private fun assertPrivacyNotice() {
"org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)
Espresso.pressBack()
}
private fun assertKnowYourRights() {
@ -192,8 +214,6 @@ private fun assertKnowYourRights() {
"org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)
Espresso.pressBack()
}
private fun assertLicensingInformation() {
@ -210,8 +230,6 @@ private fun assertLicensingInformation() {
"org.mozilla.fenix.debug:id/mozac_browser_toolbar_url_view",
R.id.mozac_browser_toolbar_url_view
)
Espresso.pressBack()
}
private fun assertLibrariesUsed() {

@ -36,9 +36,9 @@ class SettingsSubMenuDeleteBrowsingDataOnQuitRobot {
fun clickDeleteBrowsingOnQuitButtonSwitchDefaultChange() = verifyDeleteBrowsingOnQuitButtonSwitchDefault().click()
fun verifyAllTheCheckBoxesText() = assertAllTheCheckBoxesText()
fun verifyAllTheCheckBoxesText() = assertAllOptionsAndCheckBoxes()
fun verifyAllTheCheckBoxesChecked() = assertAllTheCheckBoxesChecked()
fun verifyAllTheCheckBoxesChecked() = assertAllCheckBoxesAreChecked()
fun verifyDeleteBrowsingDataOnQuitSubMenuItems() {
verifyDeleteBrowsingOnQuitButton()
@ -88,7 +88,7 @@ private fun assertDeleteBrowsingOnQuitButtonSummary() = onView(
private fun assertDeleteBrowsingOnQuitButtonSwitchDefault() = onView(withResourceName("switch_widget"))
.check(matches(isChecked(false)))
private fun assertAllTheCheckBoxesText() {
private fun assertAllOptionsAndCheckBoxes() {
onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -111,6 +111,6 @@ private fun assertAllTheCheckBoxesText() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAllTheCheckBoxesChecked() {
private fun assertAllCheckBoxesAreChecked() {
// Only verifying the options, checkboxes default value can't be verified due to issue #9471
}

@ -20,8 +20,10 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click
@ -32,42 +34,57 @@ import org.mozilla.fenix.helpers.click
class SettingsSubMenuDeleteBrowsingDataRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyDeleteBrowsingDataButton() = assertDeleteBrowsingDataButton()
fun verifyClickDeleteBrowsingDataButton() = assertClickDeleteBrowsingDataButton()
fun verifyMessageInDialogBox() = assertMessageInDialogBox()
fun verifyDeleteButtonInDialogBox() = assertDeleteButtonInDialogBox()
fun verifyCancelButtonInDialogBox() = assertCancelButtonInDialogBox()
fun verifyAllTheCheckBoxesText() = assertAllTheCheckBoxesText()
fun verifyAllTheCheckBoxesChecked() = assertAllTheCheckBoxesChecked()
fun verifyContentsInDialogBox() {
fun verifyAllOptionsAndCheckBoxes() = assertAllOptionsAndCheckBoxes()
fun verifyAllCheckBoxesAreChecked() = assertAllCheckBoxesAreChecked()
fun verifyOpenTabsCheckBox(status: Boolean) = assertOpenTabsCheckBox(status)
fun verifyBrowsingHistoryDetails(status: Boolean) = assertBrowsingHistoryCheckBox(status)
fun verifyCookiesCheckBox(status: Boolean) = assertCookiesCheckBox(status)
fun verifyCachedFilesCheckBox(status: Boolean) = assertCachedFilesCheckBox(status)
fun verifySitePermissionsCheckBox(status: Boolean) = assertSitePermissionsCheckBox(status)
fun verifyDownloadsCheckBox(status: Boolean) = assertDownloadsCheckBox(status)
fun verifyOpenTabsDetails(tabNumber: String) = assertOpenTabsDescription(tabNumber)
fun verifyBrowsingHistoryDetails(addresses: String) = assertBrowsingHistoryDescription(addresses)
fun verifyDialogElements() {
verifyMessageInDialogBox()
verifyDeleteButtonInDialogBox()
verifyCancelButtonInDialogBox()
}
fun switchOpenTabsCheckBox() = clickOpenTabsCheckBox()
fun switchBrowsingHistoryCheckBox() = clickBrowsingHistoryCheckBox()
fun switchCookiesCheckBox() = clickCookiesCheckBox()
fun switchCachedFilesCheckBox() = clickCachedFilesCheckBox()
fun switchSitePermissionsCheckBox() = clickSitePermissionsCheckBox()
fun switchDownloadsCheckBox() = clickDownloadsCheckBox()
fun clickDeleteBrowsingDataButton() = deleteBrowsingDataButton().click()
fun clickDialogCancelButton() = dialogCancelButton().click()
fun selectOnlyOpenTabsCheckBox() = checkOnlyOpenTabsCheckBox()
fun selectOnlyBrowsingHistoryCheckBox() = checkOnlyBrowsingHistoryCheckBox()
fun clickCancelButtonInDialogBoxAndVerifyContentsInDialogBox() {
mDevice.wait(
Until.findObject(By.text("Delete browsing data")),
TestAssetHelper.waitingTime
)
verifyClickDeleteBrowsingDataButton()
verifyContentsInDialogBox()
clickDeleteBrowsingDataButton()
verifyDialogElements()
cancelButton().click()
}
fun confirmDeletionAndAssertSnackbar() {
dialogDeleteButton().click()
assertDeleteBrowsingDataSnackbar()
}
fun verifyDeleteBrowsingDataSubMenuItems() {
verifyDeleteBrowsingDataButton()
clickCancelButtonInDialogBoxAndVerifyContentsInDialogBox()
verifyAllTheCheckBoxesText()
verifyAllTheCheckBoxesChecked()
verifyAllOptionsAndCheckBoxes()
verifyAllCheckBoxesAreChecked()
}
class Transition {
@ -85,82 +102,174 @@ class SettingsSubMenuDeleteBrowsingDataRobot {
private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up")))
private fun assertNavigationToolBarHeader() {
private fun navigationToolBarHeader() =
onView(
allOf(
withId(R.id.navigationToolbar),
withChild(withText(R.string.preferences_delete_browsing_data))
)
)
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun assertDeleteBrowsingDataButton() {
onView(withId(R.id.delete_data))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun deleteBrowsingDataButton() = onView(withId(R.id.delete_data))
private fun assertClickDeleteBrowsingDataButton() {
onView(withId(R.id.delete_data))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))).click()
}
private fun assertNavigationToolBarHeader() =
navigationToolBarHeader().check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun assertDeleteBrowsingDataButton() =
deleteBrowsingDataButton().check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun cancelButton() =
mDevice.findObject(UiSelector().textStartsWith("CANCEL"))
private fun assertMessageInDialogBox() =
onView(withText("$appName will delete the selected browsing data."))
.inRoot(isDialog())
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun dialogDeleteButton() = onView(withText("Delete")).inRoot(isDialog())
private fun assertDeleteButtonInDialogBox() =
onView(withText("Delete"))
.inRoot(isDialog())
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun dialogCancelButton() = onView(withText("Cancel")).inRoot(isDialog())
private fun assertCancelButtonInDialogBox() =
onView(withText("Cancel"))
.inRoot(isDialog())
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun openTabsSubsection() = onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
private fun assertAllTheCheckBoxesText() {
private fun openTabsDescription(tabNumber: String) = onView(withText("$tabNumber tabs"))
onView(withText(R.string.preferences_delete_browsing_data_tabs_title_2))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("0 tabs"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun openTabsCheckBox() = onView(allOf(withId(R.id.checkbox), hasSibling(withText("Open tabs"))))
private fun browsingHistorySubsection() =
onView(withText(R.string.preferences_delete_browsing_data_browsing_data_title))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("0 addresses"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun browsingHistoryDescription(addresses: String) = onView(withText("$addresses addresses"))
private fun browsingHistoryCheckBox() =
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Browsing history and site data"))))
private fun cookiesSubsection() =
onView(withText(R.string.preferences_delete_browsing_data_cookies))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText(R.string.preferences_delete_browsing_data_cookies_subtitle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun cookiesDescription() = onView(withText(R.string.preferences_delete_browsing_data_cookies_subtitle))
private fun cookiesCheckBox() =
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cookies"))))
private fun cachedFilesSubsection() =
onView(withText(R.string.preferences_delete_browsing_data_cached_files))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun cachedFilesDescription() =
onView(withText(R.string.preferences_delete_browsing_data_cached_files_subtitle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun cachedFilesCheckBox() =
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cached images and files"))))
private fun sitePermissionsSubsection() =
onView(withText(R.string.preferences_delete_browsing_data_site_permissions))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun sitePermissionsCheckBox() =
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Site permissions"))))
private fun downloadsSubsection() =
onView(withText(R.string.preferences_delete_browsing_data_downloads))
private fun downloadsCheckBox() =
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Downloads"))))
private fun dialogMessage() =
onView(withText("$appName will delete the selected browsing data."))
.inRoot(isDialog())
private fun assertMessageInDialogBox() =
dialogMessage().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDeleteButtonInDialogBox() =
dialogDeleteButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertCancelButtonInDialogBox() =
dialogCancelButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAllOptionsAndCheckBoxes() {
openTabsSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
openTabsDescription("0").check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
openTabsCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
browsingHistorySubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
browsingHistoryDescription("0").check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
browsingHistoryCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cookiesSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cookiesDescription().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cookiesCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cachedFilesSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cachedFilesDescription().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
cachedFilesCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
sitePermissionsSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
sitePermissionsCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
downloadsSubsection().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
downloadsCheckBox().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAllCheckBoxesAreChecked() {
openTabsCheckBox().assertIsChecked(true)
browsingHistoryCheckBox().assertIsChecked(true)
cookiesCheckBox().assertIsChecked(true)
cachedFilesCheckBox().assertIsChecked(true)
sitePermissionsCheckBox().assertIsChecked(true)
downloadsCheckBox().assertIsChecked(true)
}
private fun assertOpenTabsDescription(tabNumber: String) =
openTabsDescription(tabNumber).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertBrowsingHistoryDescription(addresses: String) =
browsingHistoryDescription(addresses).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertDeleteBrowsingDataSnackbar() {
assertTrue(
mDevice.findObject(
UiSelector().text("Browsing data deleted")
).waitUntilGone(waitingTime)
)
}
private fun assertAllTheCheckBoxesChecked() {
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Open tabs")))).assertIsChecked(true)
private fun clickOpenTabsCheckBox() = openTabsCheckBox().click()
private fun assertOpenTabsCheckBox(status: Boolean) = openTabsCheckBox().assertIsChecked(status)
private fun clickBrowsingHistoryCheckBox() = browsingHistoryCheckBox().click()
private fun assertBrowsingHistoryCheckBox(status: Boolean) = browsingHistoryCheckBox().assertIsChecked(status)
private fun clickCookiesCheckBox() = cookiesCheckBox().click()
private fun assertCookiesCheckBox(status: Boolean) = cookiesCheckBox().assertIsChecked(status)
private fun clickCachedFilesCheckBox() = cachedFilesCheckBox().click()
private fun assertCachedFilesCheckBox(status: Boolean) = cachedFilesCheckBox().assertIsChecked(status)
private fun clickSitePermissionsCheckBox() = sitePermissionsCheckBox().click()
private fun assertSitePermissionsCheckBox(status: Boolean) = sitePermissionsCheckBox().assertIsChecked(status)
private fun clickDownloadsCheckBox() = downloadsCheckBox().click()
private fun assertDownloadsCheckBox(status: Boolean) = downloadsCheckBox().assertIsChecked(status)
fun checkOnlyOpenTabsCheckBox() {
clickBrowsingHistoryCheckBox()
assertBrowsingHistoryCheckBox(false)
clickCookiesCheckBox()
assertCookiesCheckBox(false)
clickCachedFilesCheckBox()
assertCachedFilesCheckBox(false)
clickSitePermissionsCheckBox()
assertSitePermissionsCheckBox(false)
clickDownloadsCheckBox()
assertDownloadsCheckBox(false)
assertOpenTabsCheckBox(true)
}
fun checkOnlyBrowsingHistoryCheckBox() {
clickOpenTabsCheckBox()
assertOpenTabsCheckBox(false)
clickCookiesCheckBox()
assertCookiesCheckBox(false)
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Browsing history and site data")))).assertIsChecked(true)
clickCachedFilesCheckBox()
assertCachedFilesCheckBox(false)
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cookies")))).assertIsChecked(true)
clickSitePermissionsCheckBox()
assertSitePermissionsCheckBox(false)
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Cached images and files")))).assertIsChecked(true)
clickDownloadsCheckBox()
assertDownloadsCheckBox(false)
onView(allOf(withId(R.id.checkbox), hasSibling(withText("Site permissions")))).assertIsChecked(true)
assertBrowsingHistoryCheckBox(true)
}

@ -50,8 +50,6 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
fun verifyEnhancedTrackingProtectionOptions() = assertEnhancedTrackingProtectionOptions()
fun verifyEnhancedTrackingProtectionOptionsGrayedOut() = assertEnhancedTrackingProtectionOptionsGrayedOut()
fun verifyTrackingProtectionSwitchEnabled() = assertTrackingProtectionSwitchEnabled()
fun switchEnhancedTrackingProtectionToggle() = onView(withResourceName("switch_widget")).click()

@ -51,6 +51,18 @@ class SettingsSubMenuPrivateBrowsingRobot {
fun clickOpenLinksInPrivateTabSwitch() = openLinksInPrivateTabSwitch().click()
fun cancelPrivateShortcutAddition() {
mDevice.wait(
Until.findObject(text("Add private browsing shortcut")),
waitingTime
)
addPrivateBrowsingShortcutButton().click()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDevice.wait(Until.findObject(By.textContains("CANCEL")), waitingTime)
cancelShortcutAdditionButton().click()
}
}
fun addPrivateShortcutToHomescreen() {
mDevice.wait(
Until.findObject(text("Add private browsing shortcut")),
@ -105,6 +117,9 @@ private fun goBackButton() = onView(withContentDescription("Navigate up"))
private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textStartsWith("add automatically"))
private fun cancelShortcutAdditionButton() =
mDevice.findObject(UiSelector().textContains("CANCEL"))
private fun privateBrowsingShortcutIcon() = mDevice.findObject(text("Private $appName"))
private fun assertAddPrivateBrowsingShortcutButton() {

@ -8,23 +8,28 @@ package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -53,9 +58,11 @@ class SettingsSubMenuSearchRobot {
fun saveNewSearchEngine() {
addSearchEngineSaveButton().click()
mDevice.findObject(
UiSelector().resourceId("$packageName:id/recycler_view")
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector().textContains("Default search engine")
).waitForExists(waitingTime)
)
}
fun addNewSearchEngine(searchEngineName: String) {
@ -63,6 +70,35 @@ class SettingsSubMenuSearchRobot {
saveNewSearchEngine()
}
fun selectAddCustomSearchEngine() = onView(withText("Other")).click()
fun typeCustomEngineDetails(engineName: String, engineURL: String) {
onView(withId(R.id.edit_engine_name))
.perform(clearText())
.perform(typeText(engineName))
onView(withId(R.id.edit_search_string))
.perform(clearText())
.perform(typeText(engineURL))
}
fun openEngineOverflowMenu(searchEngineName: String) {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/overflow_menu")
).waitForExists(waitingTime)
threeDotMenu(searchEngineName).click()
}
fun clickEdit() = onView(withText("Edit")).click()
fun saveEditSearchEngine() {
onView(withId(R.id.save_button)).click()
assertTrue(
mDevice.findObject(
UiSelector().textContains("Saved")
).waitForExists(waitingTime)
)
}
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -78,21 +114,21 @@ class SettingsSubMenuSearchRobot {
private fun assertDefaultSearchEngineHeader() =
onView(withText("Default search engine"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertSearchEngineList() {
onView(withText("Google"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Amazon.com"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Bing"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("DuckDuckGo"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Wikipedia"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
onView(withText("Add search engine"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertShowSearchSuggestions() {
@ -102,7 +138,7 @@ private fun assertShowSearchSuggestions() {
)
)
onView(withText("Show search suggestions"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertShowSearchShortcuts() {
@ -112,7 +148,7 @@ private fun assertShowSearchShortcuts() {
)
)
onView(withText("Show search engines"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertShowClipboardSuggestions() {
@ -122,7 +158,7 @@ private fun assertShowClipboardSuggestions() {
)
)
onView(withText("Show clipboard suggestions"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertSearchBrowsingHistory() {
@ -132,7 +168,7 @@ private fun assertSearchBrowsingHistory() {
)
)
onView(withText("Search browsing history"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertSearchBookmarks() {
@ -142,12 +178,12 @@ private fun assertSearchBookmarks() {
)
)
onView(withText("Search bookmarks"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun selectSearchEngine(searchEngine: String) {
onView(withText(searchEngine))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.perform(click())
}
@ -189,3 +225,11 @@ private fun addSearchEngineSaveButton() = onView(withId(R.id.add_search_engine))
private fun assertEngineListContains(searchEngineName: String) {
onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName))))
}
private fun threeDotMenu(searchEngineName: String) =
onView(
allOf(
withId(R.id.overflow_menu),
withParent(withChild(withText(searchEngineName)))
)
)

@ -16,6 +16,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click
/**
@ -23,11 +24,16 @@ import org.mozilla.fenix.helpers.click
*/
class SettingsSubMenuTabsRobot {
fun verifyOptions() = assertOptions()
fun verifyTabViewOptions() = assertTabViewOptions()
fun verifyCloseTabsOptions() = assertCloseTabsOptions()
fun verifyStartOnHomeOptions() = assertStartOnHomeOptions()
fun clickAlwaysStartOnHomeToggle() = alwaysStartOnHomeToggle().click()
fun clickAlwaysStartOnHomeToggle() {
scrollToElementByText("Move old tabs to inactive")
alwaysStartOnHomeToggle().click()
}
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -42,11 +48,22 @@ class SettingsSubMenuTabsRobot {
}
}
private fun assertOptions() {
private fun assertTabViewOptions() {
tabViewHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
listToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
gridToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
searchTermTabGroupsToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertCloseTabsOptions() {
closeTabsHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneDayToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
manualToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneWeekToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneMonthToggle()
@ -54,17 +71,27 @@ private fun assertOptions() {
}
private fun assertStartOnHomeOptions() {
// Scroll to ensure all the items are visible.
scrollToElementByText("Never")
startOnHomeHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterFourHoursToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
alwaysStartOnHomeToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
neverStartOnHomeToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun manualToggle() = onView(withText("Manually"))
private fun tabViewHeading() = onView(withText("Tab view"))
private fun listToggle() = onView(withText("List"))
private fun gridToggle() = onView(withText("Grid"))
private fun searchTermTabGroupsToggle() = onView(withText("Search groups"))
private fun closeTabsHeading() = onView(withText("Close tabs"))
private fun manuallyToggle() = onView(withText("Manually"))
private fun afterOneDayToggle() = onView(withText("After one day"))

@ -6,14 +6,18 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
/**
@ -22,7 +26,7 @@ import org.mozilla.fenix.helpers.click
class SyncSignInRobot {
fun verifyAccountSettingsMenuHeader() = assertAccountSettingsMenuHeader()
fun verifySyncSignInMenuHeader() = assertSyncSignInMenuHeader()
fun verifyTurnOnSyncMenu() = assertTurnOnSyncMenu()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
@ -46,7 +50,13 @@ private fun assertAccountSettingsMenuHeader() {
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun assertSyncSignInMenuHeader() {
onView(withText(R.string.sign_in_with_camera))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
private fun assertTurnOnSyncMenu() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/container")).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/signInScanButton")
.resourceId("$packageName:id/signInEmailButton")
).waitForExists(waitingTime)
)
}

@ -148,16 +148,18 @@ class TabDrawerRobot {
}
fun verifyTabMediaControlButtonState(action: String) {
mDevice.waitNotNull(
findObject(
By
.res("$packageName:id/play_pause_button")
.desc(action)
),
waitingTime
)
mDevice.waitForIdle()
tabMediaControlButton().check(matches(withContentDescription(action)))
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/play_pause_button")
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector().descriptionContains(action)
).waitForExists(waitingTime)
)
}
fun clickTabMediaControlButton() = tabMediaControlButton().click()
@ -252,7 +254,7 @@ class TabDrawerRobot {
fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.waitForIdle()
newTabButton().perform(click())
newTabButton().click()
SearchRobot().interact()
return SearchRobot.Transition()
}
@ -392,7 +394,7 @@ private fun normalBrowsingButton() = onView(
)
private fun privateBrowsingButton() = onView(withContentDescription("Private tabs"))
private fun newTabButton() = onView(withId(R.id.new_tab_button))
private fun newTabButton() = mDevice.findObject(UiSelector().resourceId("$packageName:id/new_tab_button"))
private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
private fun assertExistingOpenTabs(title: String) {

@ -16,6 +16,7 @@ import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isChecked
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
@ -28,6 +29,7 @@ import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.not
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
@ -66,11 +68,6 @@ class ThreeDotMenuMainRobot {
}
fun clickShareButton() {
var maxSwipes = 3
while (!shareButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
shareButton().click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
}
@ -93,6 +90,13 @@ class ThreeDotMenuMainRobot {
fun verifyNewTabButton() = assertNewTabButton()
fun verifyReportSiteIssueButton() = assertReportSiteIssueButton()
fun verifyDesktopSiteModeEnabled(state: Boolean) {
expandMenu()
if (state) {
desktopSiteButton().check(matches(isChecked()))
} else desktopSiteButton().check(matches(not(isChecked())))
}
fun verifyPageThreeDotMainMenuItems() {
verifyNewTabButton()
verifyBookmarksButton()
@ -139,7 +143,7 @@ class ThreeDotMenuMainRobot {
}
fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
threeDotMenuRecyclerView().perform(swipeDown())
downloadsButton().click()
DownloadRobot().interact()
@ -147,7 +151,7 @@ class ThreeDotMenuMainRobot {
}
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sign in to sync")), waitingTime)
signInToSyncButton().click()
@ -156,7 +160,7 @@ class ThreeDotMenuMainRobot {
}
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click()
@ -167,7 +171,7 @@ class ThreeDotMenuMainRobot {
}
fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
threeDotMenuRecyclerView().perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
historyButton().click()
@ -185,6 +189,7 @@ class ThreeDotMenuMainRobot {
fun sharePage(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition {
shareButton().click()
LibrarySubMenusMultipleSelectionToolbarRobot().interact()
return LibrarySubMenusMultipleSelectionToolbarRobot.Transition()
}
@ -205,10 +210,7 @@ class ThreeDotMenuMainRobot {
}
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
// Close three dot
mDevice.pressBack()
// Nav back to previous page
mDevice.pressBack()
backButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -238,14 +240,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun stopPageLoad(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Stop")), waitingTime)
stopLoadingButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
closeAllTabsButton().click()
@ -254,6 +248,8 @@ class ThreeDotMenuMainRobot {
}
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
reportSiteIssueButton().click()
BrowserRobot().interact()
@ -261,7 +257,8 @@ class ThreeDotMenuMainRobot {
}
fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
findInPageButton().click()
@ -278,11 +275,8 @@ class ThreeDotMenuMainRobot {
}
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
var maxSwipes = 3
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
readerViewAppearanceToggle().click()
ReaderViewRobot().interact()
@ -305,11 +299,8 @@ class ThreeDotMenuMainRobot {
}
fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
var maxSwipes = 3
while (!installPWAButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
installPWAButton().click()
AddToHomeScreenRobot().interact()
@ -318,9 +309,8 @@ class ThreeDotMenuMainRobot {
fun openSaveToCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
// Ensure the menu is expanded and fully scrolled to the bottom.
for (i in 0..3) {
threeDotMenuRecyclerView().perform(swipeUp())
}
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click()
@ -337,6 +327,24 @@ class ThreeDotMenuMainRobot {
SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition()
}
fun clickOpenInApp(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
openInAppButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun switchDesktopSiteMode(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
threeDotMenuRecyclerView().perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun threeDotMenuRecyclerView() =
@ -373,6 +381,8 @@ private fun assertHelpButton() = helpButton()
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun assertForwardButton() = assertTrue(forwardButton().waitForExists(waitingTime))
private fun backButton() = mDevice.findObject(UiSelector().description("Back"))
private fun addBookmarkButton() = onView(allOf(withId(R.id.checkbox), withText("Add")))
private fun assertAddBookmarkButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
@ -488,13 +498,20 @@ private fun assertAddToMobileHome() {
private fun installPWAButton() = mDevice.findObject(UiSelector().text("Install"))
private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site)))
private fun desktopSiteButton() = onView(withId(R.id.switch_widget))
private fun assertDesktopSite() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
threeDotMenuRecyclerView().perform(swipeUp())
desktopSiteButton().check(matches(isDisplayed()))
}
private fun openInAppButton() =
onView(
allOf(
withText("Open in app"),
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun downloadsButton() = onView(withText(R.string.library_downloads))
private fun assertDownloadsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())

@ -1,44 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="org.mozilla.fenix"
android:sharedUserId="${sharedUserId}">
<application
android:name="org.mozilla.fenix.MigratingFenixApplication"
tools:replace="android:name">
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
<!-- Overriding the alias of the main manifest to route app launches through our
MigrationDecisionActivity which will show the migration screen before launching
into the app if needed. -->
<activity-alias
android:name="${applicationId}.App"
android:targetActivity="org.mozilla.fenix.MigrationDecisionActivity"
tools:replace="android:targetActivity" />
<activity
android:name="org.mozilla.fenix.MigrationDecisionActivity"
android:exported="false" />
<service android:name="org.mozilla.fenix.MigrationService" />
</application>
</manifest>

@ -19,28 +19,6 @@
<application
tools:replace="android:name"
android:name="org.mozilla.fenix.DebugFenixApplication">
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
</application>
</manifest>

@ -4,163 +4,56 @@
"appId": "org.mozilla.fenix",
"appName": "fenix",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "fancy-settings",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Fancy Settings"
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
"branches": [
{
"slug": "smiley",
"slug": "no-mr2",
"ratio": 0,
"feature": {
"value": {
"settings-title-punctuation": "\uD83D\uDE03"
"sections-enabled": {
"topSites": true,
"recentExplorations": true,
"recentlySaved": false,
"jumpBackIn": false,
"pocket": false
}
},
"enabled": true,
"featureId": "nimbus-validation"
"featureId": "homescreen"
}
},
{
"slug": "bundled-text",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general"
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.firefox_beta",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Text Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-icon-variables-validation-android",
"appId": "org.mozilla.fenix",
"appName": "fenix",
"channel": "nightly",
"branches": [{
"slug": "control",
"slug": "full-mr2",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "edit-menu-icon",
"ratio": 0,
"feature": {
"value": {
"settings-title": "preferences_category_general",
"settings-icon": "ic_edit"
"sections-enabled": {
"topSites": true,
"recentExplorations": true,
"recentlySaved": true,
"jumpBackIn": true,
"pocket": true
}
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.firefox_beta",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Icon Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-text-variables-validation-ios",
"appId": "org.mozilla.ios.Fennec",
"appName": "firefox_ios",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
"featureId": "homescreen"
}
},
{
"slug": "a1",
"slug": "distraction-free",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Menu/Menu.OpenSettingsAction.Title",
"settings-title-punctuation": "…"
"sections-enabled": {
"topSites": true,
"recentExplorations": false,
"recentlySaved": false,
"jumpBackIn": false,
"pocket": false
}
},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "a2",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Settings.General.SectionName",
"settings-title-punctuation": "!"
},
"enabled": true,
"featureId": "nimbus-validation"
"featureId": "homescreen"
}
}
],
@ -170,61 +63,9 @@
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
"homescreen"
],
"application": "org.mozilla.ios.Fennec",
"bucketConfig": {
"count": 0,
"start": 0,
"total": 10000,
"namespace": "nimbus-validation-2",
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Text Variables Validation",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to text in Settings",
"last_modified": 1621443780172
},
{
"slug": "feature-icon-variables-validation-ios",
"appId": "org.mozilla.ios.Fennec",
"appName": "firefox_ios",
"channel": "nightly",
"branches": [{
"slug": "control",
"ratio": 100,
"feature": {
"value": {},
"enabled": true,
"featureId": "nimbus-validation"
}
},
{
"slug": "treatment",
"ratio": 0,
"feature": {
"value": {
"settings-title": "Fancy Settings",
"settings-icon": "menu-ViewMobile"
},
"enabled": true,
"featureId": "nimbus-validation"
}
}
],
"outcomes": [],
"arguments": {},
"probeSets": [],
"startDate": null,
"targeting": "true",
"featureIds": [
"nimbus-validation"
],
"application": "org.mozilla.ios.Fennec",
"application": "org.mozilla.firefox_beta",
"bucketConfig": {
"count": 0,
"start": 0,
@ -233,12 +74,12 @@
"randomizationUnit": "nimbus_id"
},
"schemaVersion": "1.5.0",
"userFacingName": "Nimbus Icon Variables Validation",
"userFacingName": "Home screen sections test",
"referenceBranch": "control",
"proposedDuration": 14,
"isEnrollmentPaused": false,
"proposedEnrollment": 7,
"userFacingDescription": "Demonstration experiment to make trivial visible changes to icons in Settings",
"userFacingDescription": "Experiment to test the home screen configurations",
"last_modified": 1621443780172
}
]

@ -246,6 +246,27 @@
<activity android:name=".settings.account.AuthIntentReceiverActivity"
android:exported="false" />
<activity android:name=".autofill.AutofillUnlockActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillConfirmActivity"
android:exported="false"
android:theme="@style/Theme.AppCompat.Translucent" />
<activity android:name=".autofill.AutofillSearchActivity"
android:exported="false"
android:theme="@style/DialogActivityTheme" />
<service
android:name=".autofill.AutofillService"
android:label="@string/app_name"
android:permission="android.permission.BIND_AUTOFILL_SERVICE">
<intent-filter>
<action android:name="android.service.autofill.AutofillService"/>
</intent-filter>
</service>
<service android:name=".media.MediaSessionService"
android:exported="false" />

@ -20,14 +20,17 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromSettings(R.id.settingsFragment),
FromBookmarks(R.id.bookmarkFragment),
FromHistory(R.id.historyFragment),
FromHistoryMetadataGroup(R.id.historyMetadataGroupFragment),
FromTrackingProtectionExceptions(R.id.trackingProtectionExceptionsFragment),
FromAbout(R.id.aboutFragment),
FromTrackingProtection(R.id.trackingProtectionFragment),
FromTrackingProtectionDialog(R.id.trackingProtectionPanelDialogFragment),
FromSavedLoginsFragment(R.id.savedLoginsFragment),
FromAddNewDeviceFragment(R.id.addNewDeviceFragment),
FromAddSearchEngineFragment(R.id.addSearchEngineFragment),
FromEditCustomSearchEngineFragment(R.id.editCustomSearchEngineFragment),
FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromStudiesFragment(R.id.studiesFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabsTray(R.id.tabsTrayFragment),

@ -4,6 +4,10 @@
package org.mozilla.fenix
import android.content.Context
import mozilla.components.support.locale.LocaleManager
import mozilla.components.support.locale.LocaleManager.getSystemDefault
/**
* A single source for setting feature flags that are mostly based on build type.
*/
@ -19,11 +23,6 @@ object FeatureFlags {
*/
const val addressesFeature = true
/**
* Enables the Credit Cards autofill feature.
*/
const val creditCardsFeature = true
/**
* Enables WebAuthn support.
*/
@ -32,43 +31,64 @@ object FeatureFlags {
/**
* Enables the Home button in the browser toolbar to navigate back to the home screen.
*/
val showHomeButtonFeature = Config.channel.isNightlyOrDebug
const val showHomeButtonFeature = true
/**
* Enables the Start On Home feature in the settings page.
*/
val showStartOnHomeSettings = Config.channel.isNightlyOrDebug
const val showStartOnHomeSettings = true
/**
* Enables the "recent" tabs feature in the home screen.
*/
val showRecentTabsFeature = Config.channel.isNightlyOrDebug
const val showRecentTabsFeature = true
/**
* Enables recording of history metadata.
* Enables UI features based on history metadata.
*/
val historyMetadataFeature = Config.channel.isDebug
const val historyMetadataUIFeature = true
/**
* Enables the recently saved bookmarks feature in the home screen.
*/
val recentBookmarksFeature = Config.channel.isNightlyOrDebug
const val recentBookmarksFeature = true
/**
* Identifies and separates the tabs list with a secondary section containing least used tabs.
*/
val inactiveTabs = Config.channel.isNightlyOrDebug
const val inactiveTabs = true
/**
* Enables support for Android Autofill.
*
* In addition to toggling this flag, matching entries in the Android Manifest of the build
* type need to present.
* Enables showing the home screen behind the search dialog
*/
val androidAutofill = Config.channel.isNightlyOrDebug || Config.channel.isBeta
const val showHomeBehindSearch = true
/**
* Enables showing the home screen behind the search dialog
* Enables customizing the home screen
*/
const val customizeHome = true
/**
* Identifies and separates the tabs list with a group containing search term tabs.
*/
val tabGroupFeature = Config.channel.isNightlyOrDebug
/**
* Enables showing search groupings in the History.
*/
const val showHistorySearchGroups = true
/**
* Show Pocket recommended stories on home.
*/
fun isPocketRecommendationsFeatureEnabled(context: Context): Boolean {
val langTag = LocaleManager.getCurrentLocale(context)
?.toLanguageTag() ?: getSystemDefault().toLanguageTag()
return listOf("en-US", "en-CA").contains(langTag)
}
/**
* Enables showing the homescreen onboarding card.
*/
val showHomeBehindSearch = Config.channel.isNightlyOrDebug
const val showHomeOnboarding = false
}

@ -51,7 +51,6 @@ import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline
@ -65,17 +64,24 @@ import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver
import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.feature.autofill.AutofillUseCases
import mozilla.components.feature.search.ext.buildSearchUrl
import mozilla.components.feature.search.ext.waitForSelectedOrDefaultSearchEngine
import mozilla.components.service.fxa.manager.SyncEnginesStorage
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
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.components.Core
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.perf.MarkersLifecycleCallbacks
import org.mozilla.fenix.tabstray.ext.inactiveTabs
import org.mozilla.fenix.utils.Settings
/**
@ -98,7 +104,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
override fun onCreate() {
// We use start/stop instead of measure so we don't measure outside the main process.
val completeMethodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE.
val subsectionThroughGleanTimerId = PerfStartup.appOnCreateToGleanInit.start()
super.onCreate()
@ -120,8 +125,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
initializeGlean()
}
PerfStartup.appOnCreateToGleanInit.stopAndAccumulate(subsectionThroughGleanTimerId)
setupInMainProcessOnly()
// DO NOT MOVE ANYTHING BELOW THIS stop CALL.
@ -163,54 +166,51 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@CallSuper
open fun setupInMainProcessOnly() {
PerfStartup.appOnCreateToMegazordInit.measureNoInline {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run {
// Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord()
run {
// Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
setDayNightTheme()
components.strictMode.enableStrictMode(true)
warmBrowsersCache()
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlockingIncrement { megazordSetup.await() }
}
// Make sure the engine is initialized and ready to use.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
components.core.engine.warmUp()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
// Just to make sure it is impossible for any application-services pieces
// to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlockingIncrement { megazordSetup.await() }
}
}
PerfStartup.appOnCreateToSetupInMain.measureNoInline {
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
setupLeakCanary()
startMetricsIfEnabled()
setupPush()
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
registerActivityLifecycleCallbacks(MarkersLifecycleCallbacks(components.core.engine))
// Storage maintenance disabled, for now, as it was interfering with background migrations.
// See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
// if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {
// runStorageMaintenance()
// }
// Storage maintenance disabled, for now, as it was interfering with background migrations.
// See https://github.com/mozilla-mobile/fenix/issues/7227 for context.
// if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) {
// runStorageMaintenance()
// }
components.appStartReasonProvider.registerInAppOnCreate(this)
components.startupActivityLog.registerInAppOnCreate(this)
initVisualCompletenessQueueAndQueueTasks()
components.appStartReasonProvider.registerInAppOnCreate(this)
components.startupActivityLog.registerInAppOnCreate(this)
initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
}
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
}
@OptIn(DelicateCoroutinesApi::class) // GlobalScope usage
@ -249,6 +249,27 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.core.bookmarksStorage.warmUp()
components.core.passwordsStorage.warmUp()
components.core.autofillStorage.warmUp()
// Populate the top site cache to improve initial load experience
// of the home fragment when the app is launched to a tab. The actual
// database call is not expensive. However, the additional context
// switches delay rendering top sites when the cache is empty, which
// we can prevent with this.
components.core.topSitesStorage.getTopSites(
components.settings.topSitesMaxLimit,
if (components.settings.showTopFrecentSites)
FrecencyThresholdOption.SKIP_ONE_TIME_PAGES
else
null
)
// This service uses `historyStorage`, and so we can only touch it when we know
// it's safe to touch `historyStorage. By 'safe', we mainly mean that underlying
// places library will be able to load, which requires first running Megazord.init().
// The visual completeness tasks are scheduled after the Megazord.init() call.
components.core.historyMetadataService.cleanup(
System.currentTimeMillis() - Core.HISTORY_METADATA_MAX_AGE_IN_MS
)
}
SecurePrefsTelemetry(this@FenixApplication, components.analytics.experiments).startTests()
@ -621,6 +642,15 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
tabViewSetting.set(settings.getTabViewPingString())
closeTabSetting.set(settings.getTabTimeoutPingString())
inactiveTabsCount.set(browserStore.state.inactiveTabs.size.toLong())
val installSourcePackage = if (SDK_INT >= Build.VERSION_CODES.R) {
packageManager.getInstallSourceInfo(packageName).installingPackageName
} else {
@Suppress("DEPRECATION")
packageManager.getInstallerPackageName(packageName)
}
installSource.set(installSourcePackage.orEmpty())
}
with(AndroidAutofill) {
@ -647,6 +677,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
with(Preferences) {
searchSuggestionsEnabled.set(settings.shouldShowSearchSuggestions)
remoteDebuggingEnabled.set(settings.isRemoteDebuggingEnabled)
studiesEnabled.set(settings.isExperimentationEnabled)
telemetryEnabled.set(settings.isTelemetryEnabled)
browsingHistorySuggestion.set(settings.shouldShowHistorySuggestions)
bookmarksSuggestion.set(settings.shouldShowBookmarkSuggestions)
@ -700,7 +731,23 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
else -> ""
}
)
inactiveTabsEnabled.set(settings.inactiveTabsAreEnabled)
}
reportHomeScreenMetrics(settings)
}
@VisibleForTesting
internal fun reportHomeScreenMetrics(settings: Settings) {
components.analytics.experiments.register(object : NimbusInterface.Observer {
override fun onUpdatesApplied(updated: List<EnrolledExperiment>) {
CustomizeHome.jumpBackIn.set(settings.showRecentTabsFeature)
CustomizeHome.recentlySaved.set(settings.showRecentBookmarksFeature)
CustomizeHome.mostVisitedSites.set(settings.showTopFrecentSites)
CustomizeHome.recentlyVisited.set(settings.historyMetadataUIFeature)
CustomizeHome.pocket.set(settings.showPocketRecommendationsFeature)
}
})
}
protected fun recordOnInit() {

@ -7,6 +7,7 @@ package org.mozilla.fenix
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
@ -14,10 +15,11 @@ import android.os.StrictMode
import android.os.SystemClock
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.ActionMode
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ActionMode
import android.view.ViewConfiguration
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper
@ -32,14 +34,13 @@ import androidx.navigation.NavDirections
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.search.SearchEngine
@ -50,6 +51,7 @@ import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter
@ -67,7 +69,6 @@ import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -75,11 +76,11 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
import org.mozilla.fenix.browser.browsingmode.DefaultBrowsingModeManager
import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.setNavigationIcon
@ -94,8 +95,10 @@ import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.historymetadata.HistoryMetadataGroupFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.onboarding.DefaultBrowserNotificationWorker
import org.mozilla.fenix.perf.MarkersLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
@ -111,11 +114,13 @@ import org.mozilla.fenix.settings.logins.fragment.LoginDetailFragmentDirections
import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections
import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.settings.studies.StudiesFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragment
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
@ -135,6 +140,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// components requires context to access.
protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos()
private lateinit var binding: ActivityHomeBinding
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
@ -176,9 +182,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private val startupPathProvider = StartupPathProvider()
private lateinit var startupTypeTelemetry: StartupTypeTelemetry
final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measureNoInline {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity")
final override fun onCreate(savedInstanceState: Bundle?) {
// DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
val startTimeProfiler = components.core.engine.profiler?.getProfilerTime()
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
// There is disk read violations on some devices such as samsung and pixel for android 9/10
@ -188,6 +194,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onCreate(savedInstanceState)
}
// Checks if Activity is currently in PiP mode if launched from external intents, then exits it
checkAndExitPiP()
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
@ -200,12 +209,14 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch()
setContentView(R.layout.activity_home)
binding = ActivityHomeBinding.inflate(layoutInflater)
setContentView(binding.root)
ProfilerMarkers.addListenerForOnGlobalLayout(components.core.engine, this, binding.root)
// Must be after we set the content view
if (isVisuallyComplete) {
components.performance.visualCompletenessQueue
.attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
.attachViewToRunVisualCompletenessQueueLater(WeakReference(binding.rootContainer))
}
privateNotificationObserver = PrivateNotificationFeature(
@ -241,10 +252,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
supportActionBar?.hide()
lifecycle.addObservers(
webExtensionPopupFeature,
StartupTimeline.homeActivityLifecycleObserver
)
lifecycle.addObservers(webExtensionPopupFeature)
if (shouldAddToRecentsScreen(intent)) {
intent.removeExtra(START_IN_RECENTS_SCREEN)
@ -261,9 +269,24 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.core.requestInterceptor.setNavigationController(navHost.navController)
if (settings().showPocketRecommendationsFeature) {
components.core.pocketStoriesService.startPeriodicStoriesRefresh()
}
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "HomeActivity.onCreate"
)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
private fun checkAndExitPiP() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
// Exit PiP mode
moveTaskToBack(false)
startActivity(Intent(this, this::class.java).setFlags(FLAG_ACTIVITY_REORDER_TO_FRONT))
}
}
private fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent) {
// We intentionally only record this in HomeActivity and not ExternalBrowserActivity (e.g.
// PWAs) so we don't include more unpredictable code paths in the results.
@ -271,7 +294,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.performance.visualCompletenessQueue,
components.startupStateProvider,
safeIntent,
rootContainer
binding.rootContainer
)
}
@ -279,14 +302,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
override fun onResume() {
super.onResume()
// Even if screenshots are allowed, we hide private content in the recents screen in onPause
// only when we are in private mode, so in onResume we should go back to setting these flags
// with the user screenshot setting only when we are in private mode.
// See https://github.com/mozilla-mobile/fenix/issues/11153
if (settings().lastKnownMode == BrowsingMode.Private) {
updateSecureWindowFlags(settings().lastKnownMode)
}
// Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb(
@ -310,7 +325,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
isFenixTheDefaultBrowser()
}
override fun onStart() = PerfStartup.homeActivityOnStart.measureNoInline {
override fun onStart() {
// DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
val startProfilerTime = components.core.engine.profiler?.getProfilerTime()
super.onStart()
// Diagnostic breadcrumb for "Display already aquired" crash:
@ -319,7 +337,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
message = "onStart()"
)
ProfilerMarkers.homeActivityOnStart(rootContainer, components.core.engine.profiler)
ProfilerMarkers.homeActivityOnStart(binding.rootContainer, components.core.engine.profiler)
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startProfilerTime, "HomeActivity.onStart"
) // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL.
}
override fun onStop() {
@ -340,13 +361,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
settings().shouldReturnToBrowser =
components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
// Even if screenshots are allowed, we want to hide private content in the recents screen
// only when we are in private mode
// See https://github.com/mozilla-mobile/fenix/issues/11153
if (settings().lastKnownMode.isPrivate) {
window.addFlags(FLAG_SECURE)
}
lifecycleScope.launch(IO) {
components.core.bookmarksStorage.getTree(BookmarkRoot.Root.id, true)?.let {
val desktopRootNode = DesktopFolders(
@ -411,6 +425,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
)
)
components.core.pocketStoriesService.stopPeriodicStoriesRefresh()
privateNotificationObserver?.stop()
}
@ -569,6 +584,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return false
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
ProfilerMarkers.addForDispatchTouchEvent(components.core.engine.profiler, ev)
return super.dispatchTouchEvent(ev)
}
final override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// Inspired by https://searchfox.org/mozilla-esr68/source/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java#584-613
// Android N and Huawei devices have broken onKeyLongPress events for the back button, so we
@ -667,7 +687,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
*/
override fun getSupportActionBarAndInflateIfNecessary(): ActionBar {
if (!isToolbarInflated) {
navigationToolbar = navigationToolbarStub.inflate() as Toolbar
navigationToolbar = binding.navigationToolbarStub.inflate() as Toolbar
setSupportActionBar(navigationToolbar)
// Add ids to this that we don't want to have a toolbar back button
@ -709,10 +729,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
engine: SearchEngine? = null,
forceSearch: Boolean = false,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
requestDesktopMode: Boolean = false
requestDesktopMode: Boolean = false,
historyMetadata: HistoryMetadataKey? = null
) {
openToBrowser(from, customTabSessionId)
load(searchTermOrURL, newTab, engine, forceSearch, flags, requestDesktopMode)
load(searchTermOrURL, newTab, engine, forceSearch, flags, requestDesktopMode, historyMetadata)
}
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
@ -740,12 +761,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
BookmarkFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistory ->
HistoryFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromHistoryMetadataGroup ->
HistoryMetadataGroupFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionExceptions ->
TrackingProtectionExceptionsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAbout ->
AboutFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtection ->
TrackingProtectionFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTrackingProtectionDialog ->
TrackingProtectionPanelDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromSavedLoginsFragment ->
SavedLoginsAuthFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromAddNewDeviceFragment ->
@ -764,12 +789,17 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromStudiesFragment -> StudiesFragmentDirections.actionGlobalBrowser(
customTabSessionId
)
}
/**
* Loads a URL or performs a search (depending on the value of [searchTermOrURL]).
*
* @param flags Flags that will be used when loading the URL (not applied to searches).
* @param historyMetadata The [HistoryMetadataKey] of the new tab in case this tab
* was opened from history.
*/
private fun load(
searchTermOrURL: String,
@ -777,7 +807,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
engine: SearchEngine?,
forceSearch: Boolean,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
requestDesktopMode: Boolean = false
requestDesktopMode: Boolean = false,
historyMetadata: HistoryMetadataKey? = null
) {
val startTime = components.core.engine.profiler?.getProfilerTime()
val mode = browsingModeManager.mode
@ -795,7 +826,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.useCases.tabsUseCases.addTab(
url = searchTermOrURL.toNormalizedUrl(),
flags = flags,
private = private
private = private,
historyMetadata = historyMetadata
)
} else {
components.useCases.sessionUseCases.loadUrl(

@ -20,6 +20,7 @@ import org.mozilla.fenix.components.getType
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.MarkersLifecycleCallbacks
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
@ -30,8 +31,8 @@ class IntentReceiverActivity : Activity() {
@VisibleForTesting
override fun onCreate(savedInstanceState: Bundle?) {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "IntentReceiverActivity")
// DO NOT MOVE ANYTHING ABOVE THIS getProfilerTime CALL.
val startTimeProfiler = components.core.engine.profiler?.getProfilerTime()
// StrictMode violation on certain devices such as Samsung
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
@ -45,6 +46,9 @@ class IntentReceiverActivity : Activity() {
intent.stripUnwantedFlags()
processIntent(intent)
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "IntentReceiverActivity.onCreate"
)
StartupTimeline.onActivityCreateEndIntentReceiver() // DO NOT MOVE ANYTHING BELOW HERE.
}

@ -13,12 +13,11 @@ import android.view.View
import androidx.core.net.toUri
import androidx.core.text.HtmlCompat
import androidx.core.text.getSpans
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_add_on_details.*
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translateDescription
import mozilla.components.feature.addons.ui.updatedAtDate
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnDetailsBinding
import java.text.DateFormat
import java.text.NumberFormat
import java.util.Locale
@ -39,10 +38,10 @@ interface AddonDetailsInteractor {
/**
* Shows the details of an add-on.
*/
class AddonDetailsView(
override val containerView: View,
class AddonDetailsBindingDelegate(
private val binding: FragmentAddOnDetailsBinding,
private val interactor: AddonDetailsInteractor
) : LayoutContainer {
) {
private val dateFormatter = DateFormat.getDateInstance()
private val numberFormatter = NumberFormat.getNumberInstance(Locale.getDefault())
@ -58,24 +57,24 @@ class AddonDetailsView(
private fun bindRating(addon: Addon) {
addon.rating?.let { rating ->
val resources = containerView.resources
val resources = binding.root.resources
val ratingContentDescription =
resources.getString(R.string.mozac_feature_addons_rating_content_description)
rating_view.contentDescription = String.format(ratingContentDescription, rating.average)
rating_view.rating = rating.average
binding.ratingView.contentDescription = String.format(ratingContentDescription, rating.average)
binding.ratingView.rating = rating.average
users_count.text = numberFormatter.format(rating.reviews)
binding.usersCount.text = numberFormatter.format(rating.reviews)
}
}
private fun bindWebsite(addon: Addon) {
home_page_label.setOnClickListener {
binding.homePageLabel.setOnClickListener {
interactor.openWebsite(addon.siteUrl.toUri())
}
}
private fun bindLastUpdated(addon: Addon) {
last_updated_text.text = dateFormatter.format(addon.updatedAtDate)
binding.lastUpdatedText.text = dateFormatter.format(addon.updatedAtDate)
}
private fun bindVersion(addon: Addon) {
@ -83,24 +82,24 @@ class AddonDetailsView(
if (version.isNullOrEmpty()) {
version = addon.version
}
version_text.text = version
binding.versionText.text = version
if (addon.isInstalled()) {
version_text.setOnLongClickListener {
binding.versionText.setOnLongClickListener {
interactor.showUpdaterDialog(addon)
true
}
} else {
version_text.setOnLongClickListener(null)
binding.versionText.setOnLongClickListener(null)
}
}
private fun bindAuthors(addon: Addon) {
author_text.text = addon.authors.joinToString { author -> author.name }.trim()
binding.authorText.text = addon.authors.joinToString { author -> author.name }.trim()
}
private fun bindDetails(addon: Addon) {
val detailsText = addon.translateDescription(containerView.context)
val detailsText = addon.translateDescription(binding.root.context)
val parsedText = detailsText.replace("\n", "<br/>")
val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)
@ -110,8 +109,8 @@ class AddonDetailsView(
for (link in links) {
addActionToLinks(spannableStringBuilder, link)
}
details.text = spannableStringBuilder
details.movementMethod = LinkMovementMethod.getInstance()
binding.details.text = spannableStringBuilder
binding.details.movementMethod = LinkMovementMethod.getInstance()
}
private fun addActionToLinks(

@ -21,6 +21,7 @@ import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemp
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnDetailsBinding
import org.mozilla.fenix.ext.showToolbar
/**
@ -38,7 +39,8 @@ class AddonDetailsFragment : Fragment(R.layout.fragment_add_on_details), AddonDe
showToolbar(title = args.addon.translateName(it))
}
AddonDetailsView(view, interactor = this).bind(args.addon)
val binding = FragmentAddOnDetailsBinding.bind(view)
AddonDetailsBindingDelegate(binding, interactor = this).bind(args.addon)
}
override fun openWebsite(addonSiteUrl: Uri) {

@ -10,9 +10,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.*
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnInternalSettingsBinding
import org.mozilla.fenix.ext.showToolbar
/**
@ -40,10 +40,10 @@ class AddonInternalSettingsFragment : AddonPopupBaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentAddOnInternalSettingsBinding.bind(view)
args.addon.installedState?.optionsPageUrl?.let {
engineSession?.let { engineSession ->
addonSettingsEngineView.render(engineSession)
binding.addonSettingsEngineView.render(engineSession)
engineSession.loadUrl(it)
}
} ?: findNavController().navigateUp()

@ -5,14 +5,12 @@
package org.mozilla.fenix.addons
import android.net.Uri
import android.view.View
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_add_on_permissions.*
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonPermissionsAdapter
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
import org.mozilla.fenix.theme.ThemeManager
interface AddonPermissionsDetailsInteractor {
@ -26,10 +24,10 @@ interface AddonPermissionsDetailsInteractor {
/**
* Shows the permission details of an add-on.
*/
class AddonPermissionsDetailsView(
override val containerView: View,
class AddonPermissionDetailsBindingDelegate(
val binding: FragmentAddOnPermissionsBinding,
private val interactor: AddonPermissionsDetailsInteractor
) : LayoutContainer {
) {
fun bind(addon: Addon) {
bindPermissions(addon)
@ -37,7 +35,7 @@ class AddonPermissionsDetailsView(
}
private fun bindPermissions(addon: Addon) {
add_ons_permissions.apply {
binding.addOnsPermissions.apply {
layoutManager = LinearLayoutManager(context)
val sortedPermissions = addon.translatePermissions(context).sorted()
adapter = AddonPermissionsAdapter(
@ -50,7 +48,7 @@ class AddonPermissionsDetailsView(
}
private fun bindLearnMore() {
learn_more_label.setOnClickListener {
binding.learnMoreLabel.setOnClickListener {
interactor.openWebsite(LEARN_MORE_URL.toUri())
}
}

@ -13,6 +13,7 @@ import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnPermissionsBinding
import org.mozilla.fenix.ext.showToolbar
/**
@ -29,7 +30,8 @@ class AddonPermissionsDetailsFragment :
context?.let {
showToolbar(args.addon.translateName(it))
}
AddonPermissionsDetailsView(view, interactor = this).bind(args.addon)
val binding = FragmentAddOnPermissionsBinding.bind(view)
AddonPermissionDetailsBindingDelegate(binding, interactor = this).bind(args.addon)
}
override fun openWebsite(addonSiteUrl: Uri) {

@ -26,9 +26,6 @@ import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_add_ons_management.addonProgressOverlay
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_empty_message
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_list
import kotlinx.android.synthetic.main.fragment_add_ons_management.view.add_ons_progress_bar
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.add_ons_overlay_text
import kotlinx.android.synthetic.main.overlay_add_on_progress.view.cancel_button
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
@ -41,6 +38,7 @@ import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.FragmentAddOnsManagementBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
@ -60,6 +58,8 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private val args by navArgs<AddonsManagementFragmentArgs>()
private var binding: FragmentAddOnsManagementBinding? = null
/**
* Whether or not an add-on installation is in progress.
*/
@ -88,7 +88,8 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindRecyclerView(view)
binding = FragmentAddOnsManagementBinding.bind(view)
bindRecyclerView()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -161,16 +162,17 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
super.onDestroyView()
// letting go of the resources to avoid memory leak.
adapter = null
binding = null
}
private fun bindRecyclerView(view: View) {
private fun bindRecyclerView() {
val managementView = AddonsManagementView(
navController = findNavController(),
showPermissionDialog = ::showPermissionDialog
)
val recyclerView = view.add_ons_list
recyclerView.layoutManager = LinearLayoutManager(requireContext())
val recyclerView = binding?.addOnsList
recyclerView?.layoutManager = LinearLayoutManager(requireContext())
val shouldRefresh = adapter != null
// If the fragment was launched to install an "external" add-on from AMO, we deactivate
@ -190,10 +192,10 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
)
}
isInstallationInProgress = false
view.add_ons_progress_bar.isVisible = false
view.add_ons_empty_message.isVisible = false
binding?.addOnsProgressBar?.isVisible = false
binding?.addOnsEmptyMessage?.isVisible = false
recyclerView.adapter = adapter
recyclerView?.adapter = adapter
if (shouldRefresh) {
adapter?.updateAddons(addons!!)
}
@ -208,13 +210,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
} catch (e: AddonManagerException) {
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
showSnackBar(
view,
getString(R.string.mozac_feature_addons_failed_to_query_add_ons)
)
binding?.let {
showSnackBar(it.root, getString(R.string.mozac_feature_addons_failed_to_query_add_ons))
}
isInstallationInProgress = false
view.add_ons_progress_bar.isVisible = false
view.add_ons_empty_message.isVisible = true
binding?.addOnsProgressBar?.isVisible = false
binding?.addOnsEmptyMessage?.isVisible = true
}
}
}
@ -341,10 +342,10 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
}
private val onPositiveButtonClicked: ((Addon) -> Unit) = { addon ->
addonProgressOverlay?.visibility = View.VISIBLE
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.VISIBLE
if (requireContext().settings().accessibilityServicesEnabled) {
announceForAccessibility(addonProgressOverlay.add_ons_overlay_text.text)
binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
}
isInstallationInProgress = true
@ -355,7 +356,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
runIfFragmentIsAttached {
isInstallationInProgress = false
adapter?.updateAddon(it)
addonProgressOverlay?.visibility = View.GONE
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
showInstallationDialog(it)
}
},
@ -374,17 +375,18 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
)
}
}
addonProgressOverlay?.visibility = View.GONE
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
isInstallationInProgress = false
}
}
)
addonProgressOverlay.cancel_button.setOnClickListener {
binding?.addonProgressOverlay?.cancelButton?.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
val safeBinding = binding
// Hide the installation progress overlay once cancellation is successful.
if (installOperation.cancel().await()) {
addonProgressOverlay.visibility = View.GONE
safeBinding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
}
}
@ -394,10 +396,14 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
val event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT
)
addonProgressOverlay.onInitializeAccessibilityEvent(event)
binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event)
event.text.add(announcementText)
event.contentDescription = null
addonProgressOverlay.parent.requestSendAccessibilityEvent(addonProgressOverlay, event)
binding?.addonProgressOverlay?.overlayCardView?.parent?.requestSendAccessibilityEvent(
binding?.addonProgressOverlay?.overlayCardView,
event
)
}
companion object {

@ -15,7 +15,6 @@ import androidx.navigation.Navigation
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.google.android.material.switchmaterial.SwitchMaterial
import kotlinx.android.synthetic.main.fragment_installed_add_on_details.view.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
@ -24,6 +23,7 @@ import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.FragmentInstalledAddOnDetailsBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -34,6 +34,8 @@ import org.mozilla.fenix.ext.runIfFragmentIsAttached
@Suppress("LargeClass", "TooManyFunctions")
class InstalledAddonDetailsFragment : Fragment() {
private lateinit var addon: Addon
private var _binding: FragmentInstalledAddOnDetailsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
@ -44,17 +46,28 @@ class InstalledAddonDetailsFragment : Fragment() {
addon = AddonDetailsFragmentArgs.fromBundle(requireNotNull(arguments)).addon
}
return inflater.inflate(R.layout.fragment_installed_add_on_details, container, false).also {
bindUI(it)
}
_binding = FragmentInstalledAddOnDetailsBinding.inflate(
inflater,
container,
false
)
bindUI()
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
bindAddon(view)
bindAddon()
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun bindAddon(view: View) {
private fun bindAddon() {
lifecycleScope.launch(Dispatchers.IO) {
try {
val addons = requireContext().components.addonManager.getAddons()
@ -65,10 +78,10 @@ class InstalledAddonDetailsFragment : Fragment() {
throw AddonManagerException(Exception("Addon ${addon.id} not found"))
} else {
addon = it
bindUI(view)
bindUI()
}
view.add_on_progress_bar.isVisible = false
view.addon_container.isVisible = true
binding.addOnProgressBar.isVisible = false
binding.addonContainer.isVisible = true
}
}
}
@ -76,7 +89,7 @@ class InstalledAddonDetailsFragment : Fragment() {
lifecycleScope.launch(Dispatchers.Main) {
runIfFragmentIsAttached {
showSnackBar(
view,
binding.root,
getString(R.string.mozac_feature_addons_failed_to_query_add_ons)
)
findNavController().popBackStack()
@ -86,27 +99,27 @@ class InstalledAddonDetailsFragment : Fragment() {
}
}
private fun bindUI(view: View) {
val title = addon.translateName(view.context)
private fun bindUI() {
val title = addon.translateName(binding.root.context)
showToolbar(title)
bindEnableSwitch(view)
bindSettings(view)
bindDetails(view)
bindPermissions(view)
bindAllowInPrivateBrowsingSwitch(view)
bindRemoveButton(view)
bindEnableSwitch()
bindSettings()
bindDetails()
bindPermissions()
bindAllowInPrivateBrowsingSwitch()
bindRemoveButton()
}
@SuppressWarnings("LongMethod")
private fun bindEnableSwitch(view: View) {
val switch = view.enable_switch
val privateBrowsingSwitch = view.allow_in_private_browsing_switch
private fun bindEnableSwitch() {
val switch = binding.enableSwitch
val privateBrowsingSwitch = binding.allowInPrivateBrowsingSwitch
switch.setState(addon.isEnabled())
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
view.remove_add_on.isEnabled = false
binding.removeAddOn.isEnabled = false
if (isChecked) {
addonManager.enableAddon(
addon,
@ -117,11 +130,11 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isVisible = it.isEnabled()
privateBrowsingSwitch.isChecked = it.isAllowedInPrivateBrowsing()
switch.setText(R.string.mozac_feature_addons_enabled)
view.settings.isVisible = shouldSettingsBeVisible()
view.remove_add_on.isEnabled = true
binding.settings.isVisible = shouldSettingsBeVisible()
binding.removeAddOn.isEnabled = true
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_successfully_enabled,
addon.translateName(it)
@ -133,11 +146,11 @@ class InstalledAddonDetailsFragment : Fragment() {
onError = {
runIfFragmentIsAttached {
switch.isClickable = true
view.remove_add_on.isEnabled = true
binding.removeAddOn.isEnabled = true
switch.setState(addon.isEnabled())
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_enable,
addon.translateName(it)
@ -148,7 +161,7 @@ class InstalledAddonDetailsFragment : Fragment() {
}
)
} else {
view.settings.isVisible = false
binding.settings.isVisible = false
addonManager.disableAddon(
addon,
onSuccess = {
@ -157,10 +170,10 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.isClickable = true
privateBrowsingSwitch.isVisible = it.isEnabled()
switch.setText(R.string.mozac_feature_addons_disabled)
view.remove_add_on.isEnabled = true
binding.removeAddOn.isEnabled = true
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_successfully_disabled,
addon.translateName(it)
@ -173,11 +186,11 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached {
switch.isClickable = true
privateBrowsingSwitch.isClickable = true
view.remove_add_on.isEnabled = true
binding.removeAddOn.isEnabled = true
switch.setState(addon.isEnabled())
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_disable,
addon.translateName(it)
@ -191,8 +204,8 @@ class InstalledAddonDetailsFragment : Fragment() {
}
}
private fun bindSettings(view: View) {
view.settings.apply {
private fun bindSettings() {
binding.settings.apply {
isVisible = shouldSettingsBeVisible()
setOnClickListener {
requireContext().components.analytics.metrics.track(
@ -216,34 +229,34 @@ class InstalledAddonDetailsFragment : Fragment() {
}
}
private fun bindDetails(view: View) {
view.details.setOnClickListener {
private fun bindDetails() {
binding.details.setOnClickListener {
val directions =
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon
)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(binding.root).navigate(directions)
}
}
private fun bindPermissions(view: View) {
view.permissions.setOnClickListener {
private fun bindPermissions() {
binding.permissions.setOnClickListener {
val directions =
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon
)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(binding.root).navigate(directions)
}
}
private fun bindAllowInPrivateBrowsingSwitch(view: View) {
val switch = view.allow_in_private_browsing_switch
private fun bindAllowInPrivateBrowsingSwitch() {
val switch = binding.allowInPrivateBrowsingSwitch
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.isVisible = addon.isEnabled()
switch.setOnCheckedChangeListener { v, isChecked ->
val addonManager = v.context.components.addonManager
switch.isClickable = false
view.remove_add_on.isEnabled = false
binding.removeAddOn.isEnabled = false
addonManager.setAddonAllowedInPrivateBrowsing(
addon,
isChecked,
@ -251,45 +264,45 @@ class InstalledAddonDetailsFragment : Fragment() {
runIfFragmentIsAttached {
this.addon = it
switch.isClickable = true
view.remove_add_on.isEnabled = true
binding.removeAddOn.isEnabled = true
}
},
onError = {
runIfFragmentIsAttached {
switch.isChecked = addon.isAllowedInPrivateBrowsing()
switch.isClickable = true
view.remove_add_on.isEnabled = true
binding.removeAddOn.isEnabled = true
}
}
)
}
}
private fun bindRemoveButton(view: View) {
view.remove_add_on.setOnClickListener {
setAllInteractiveViewsClickable(view, false)
private fun bindRemoveButton() {
binding.removeAddOn.setOnClickListener {
setAllInteractiveViewsClickable(binding, false)
requireContext().components.addonManager.uninstallAddon(
addon,
onSuccess = {
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true)
setAllInteractiveViewsClickable(binding, true)
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_successfully_uninstalled,
addon.translateName(it)
)
)
}
view.findNavController().popBackStack()
binding.root.findNavController().popBackStack()
}
},
onError = { _, _ ->
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true)
setAllInteractiveViewsClickable(binding, true)
context?.let {
showSnackBar(
view,
binding.root,
getString(
R.string.mozac_feature_addons_failed_to_uninstall,
addon.translateName(it)
@ -302,12 +315,15 @@ class InstalledAddonDetailsFragment : Fragment() {
}
}
private fun setAllInteractiveViewsClickable(view: View, clickable: Boolean) {
view.enable_switch.isClickable = clickable
view.settings.isClickable = clickable
view.details.isClickable = clickable
view.permissions.isClickable = clickable
view.remove_add_on.isClickable = clickable
private fun setAllInteractiveViewsClickable(
binding: FragmentInstalledAddOnDetailsBinding,
clickable: Boolean
) {
binding.enableSwitch.isClickable = clickable
binding.settings.isClickable = clickable
binding.details.isClickable = clickable
binding.permissions.isClickable = clickable
binding.removeAddOn.isClickable = clickable
}
private fun SwitchMaterial.setState(checked: Boolean) {

@ -12,10 +12,10 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.synthetic.main.fragment_not_yet_supported_addons.view.*
import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapter
import mozilla.components.feature.addons.ui.UnsupportedAddonsAdapterDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentNotYetSupportedAddonsBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar
@ -40,12 +40,14 @@ class NotYetSupportedAddonFragment :
addons = args.addons.toList()
)
view.unsupported_add_ons_list.apply {
val binding = FragmentNotYetSupportedAddonsBinding.bind(view)
binding.unsupportedAddOnsList.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = unsupportedAddonsAdapter
}
view.learn_more_label.setOnClickListener {
binding.learnMoreLabel.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW).setData(Uri.parse(LEARN_MORE_URL))
startActivity(intent)
}

@ -10,13 +10,13 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FragmentAddOnInternalSettingsBinding
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.showToolbar
@ -57,10 +57,12 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentAddOnInternalSettingsBinding.bind(view)
val session = engineSession
// If we have the session, render it otherwise consume it from the store.
if (session != null) {
addonSettingsEngineView.render(session)
binding.addonSettingsEngineView.render(session)
consumePopupSession()
} else {
consumeFrom(coreComponents.store) { state ->
@ -68,7 +70,7 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession.
val popupSession = extState.popupSession
if (popupSession != null) {
initializeSession(popupSession)
addonSettingsEngineView.render(popupSession)
binding.addonSettingsEngineView.render(popupSession)
popupSession.register(this)
consumePopupSession()
engineSession = popupSession

@ -0,0 +1,81 @@
/* 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.android
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.appcompat.view.ContextThemeWrapper
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import org.mozilla.fenix.HomeActivity
/**
* Base [AppCompatDialogFragment] that adds behaviour to create a top or bottom dialog.
*/
abstract class FenixDialogFragment : AppCompatDialogFragment() {
/**
* Indicates the position of the dialog top or bottom.
*/
abstract val gravity: Int
/**
* The layout id that will be render on the dialog.
*/
abstract val layoutId: Int
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return if (gravity == Gravity.BOTTOM) {
BottomSheetDialog(requireContext(), this.theme).apply {
setOnShowListener {
val bottomSheet =
findViewById<View>(R.id.design_bottom_sheet) as FrameLayout
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
} else {
Dialog(requireContext()).applyCustomizationsForTopDialog(inflateRootView())
}
}
private fun Dialog.applyCustomizationsForTopDialog(rootView: View): Dialog {
addContentView(
rootView,
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
)
)
window?.apply {
setGravity(gravity)
setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// This must be called after addContentView, or it won't fully fill to the edge.
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
return this
}
fun inflateRootView(container: ViewGroup? = null): View {
val contextThemeWrapper = ContextThemeWrapper(
activity,
(activity as HomeActivity).themeManager.currentThemeResource
)
return LayoutInflater.from(contextThemeWrapper).inflate(
layoutId,
container,
false
)
}
}

@ -30,8 +30,6 @@ import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -115,7 +113,6 @@ import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
@ -133,10 +130,9 @@ import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import mozilla.components.support.ktx.kotlin.getOrigin
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.components.toolbar.interactor.DefaultBrowserToolbarInteractor
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.databinding.FragmentBrowserBinding
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
@ -155,6 +151,9 @@ abstract class BaseBrowserFragment :
OnBackLongPressedListener,
AccessibilityManager.AccessibilityStateChangeListener {
private var _binding: FragmentBrowserBinding? = null
protected val binding get() = _binding!!
private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
@ -212,7 +211,7 @@ abstract class BaseBrowserFragment :
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View = PerfStartup.baseBfragmentOnCreateView.measureNoInline {
): View {
customTabSessionId = requireArguments().getString(EXTRA_SESSION_ID)
// Diagnostic breadcrumb for "Display already aquired" crash:
@ -224,7 +223,7 @@ abstract class BaseBrowserFragment :
)
)
val view = inflater.inflate(R.layout.fragment_browser, container, false)
_binding = FragmentBrowserBinding.inflate(inflater, container, false)
val activity = activity as HomeActivity
activity.themeManager.applyStatusBarTheme(activity)
@ -235,30 +234,28 @@ abstract class BaseBrowserFragment :
)
}
view
return binding.root
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.baseBfragmentOnViewCreated.measureNoInline { // weird indentation to avoid breaking blame.
initializeUI(view)
if (customTabSessionId == null) {
// We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to
// this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController())
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
initializeUI(view)
observeTabSelection(requireComponents.core.store)
if (customTabSessionId == null) {
// We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to
// this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController())
}
if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store)
}
observeTabSelection(requireComponents.core.store)
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
Unit
if (!onboarding.userHasBeenOnboarded()) {
observeTabSource(requireComponents.core.store)
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
private fun initializeUI(view: View) {
val tab = getCurrentTab()
browserInitialized = if (tab != null) {
@ -281,8 +278,8 @@ abstract class BaseBrowserFragment :
browserAnimator = BrowserAnimator(
fragment = WeakReference(this),
engineView = WeakReference(engineView),
swipeRefresh = WeakReference(swipeRefresh),
engineView = WeakReference(binding.engineView),
swipeRefresh = WeakReference(binding.swipeRefresh),
viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope)
).apply {
beginAnimateInIfNecessary()
@ -295,7 +292,7 @@ abstract class BaseBrowserFragment :
val readerMenuController = DefaultReaderModeController(
readerViewFeature,
view.readerViewControlsBar,
binding.readerViewControlsBar,
isPrivate = activity.browsingModeManager.mode.isPrivate,
onReaderModeChanged = { activity.finishActionMode() }
)
@ -306,7 +303,7 @@ abstract class BaseBrowserFragment :
navController = findNavController(),
metrics = requireComponents.analytics.metrics,
readerModeController = readerMenuController,
engineView = engineView,
engineView = binding.engineView,
homeViewModel = homeViewModel,
customTabSessionId = customTabSessionId,
onTabCounterClicked = {
@ -326,7 +323,7 @@ abstract class BaseBrowserFragment :
}
viewLifecycleOwner.lifecycleScope.allowUndo(
requireView().browserLayout,
binding.browserLayout,
snackbarMessage,
requireContext().getString(R.string.snackbar_deleted_undo),
{
@ -346,7 +343,7 @@ abstract class BaseBrowserFragment :
readerModeController = readerMenuController,
sessionFeature = sessionFeature,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
swipeRefresh = swipeRefresh,
swipeRefresh = binding.swipeRefresh,
browserAnimator = browserAnimator,
customTabSessionId = customTabSessionId,
openInFenixIntent = openInFenixIntent,
@ -367,7 +364,7 @@ abstract class BaseBrowserFragment :
)
_browserToolbarView = BrowserToolbarView(
container = view.browserLayout,
container = binding.browserLayout,
toolbarPosition = context.settings().toolbarPosition,
interactor = browserToolbarInteractor,
customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) },
@ -384,8 +381,8 @@ abstract class BaseBrowserFragment :
feature = FindInPageIntegration(
store = store,
sessionId = customTabSessionId,
stub = view.stubFindInPage,
engineView = engineView,
stub = binding.stubFindInPage,
engineView = binding.engineView,
toolbarInfo = FindInPageIntegration.ToolbarInfo(
browserToolbarView.view,
!context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled,
@ -400,17 +397,12 @@ abstract class BaseBrowserFragment :
showQuickSettingsDialog()
}
browserToolbarView.view.display.setOnTrackingProtectionClickedListener {
context.metrics.track(Event.TrackingProtectionIconPressed)
showTrackingProtectionPanel()
}
contextMenuFeature.set(
feature = ContextMenuFeature(
fragmentManager = parentFragmentManager,
store = store,
candidates = getContextMenuCandidates(context, view.browserLayout),
engineView = view.engineView,
candidates = getContextMenuCandidates(context, binding.browserLayout),
engineView = binding.engineView,
useCases = context.components.useCases.contextMenuUseCases,
tabId = customTabSessionId
),
@ -492,14 +484,14 @@ abstract class BaseBrowserFragment :
)
val dynamicDownloadDialog = DynamicDownloadDialog(
container = view.browserLayout,
context = context,
downloadState = downloadState,
didFail = downloadJobStatus == DownloadState.Status.FAILED,
tryAgain = downloadFeature::tryAgain,
onCannotOpenFile = {
showCannotOpenFileError(view.browserLayout, context, it)
showCannotOpenFileError(binding.browserLayout, context, it)
},
view = view.viewDynamicDownloadDialog,
binding = binding.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight
) { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
@ -510,7 +502,7 @@ abstract class BaseBrowserFragment :
resumeDownloadDialogState(
getCurrentTab()?.id,
store, view, context, toolbarHeight
store, context, toolbarHeight
)
shareDownloadsFeature.set(
@ -594,7 +586,7 @@ abstract class BaseBrowserFragment :
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
},
loginPickerView = loginSelectBar,
loginPickerView = binding.loginSelectBar,
onManageLogins = {
browserAnimator.captureEngineViewAndDrawStatically {
val directions =
@ -602,7 +594,7 @@ abstract class BaseBrowserFragment :
findNavController().navigate(directions)
}
},
creditCardPickerView = creditCardSelectBar,
creditCardPickerView = binding.creditCardSelectBar,
onManageCreditCards = {
val directions =
NavGraphDirections.actionGlobalCreditCardsSettingFragment()
@ -620,7 +612,7 @@ abstract class BaseBrowserFragment :
feature = SessionFeature(
requireComponents.core.store,
requireComponents.useCases.sessionUseCases.goBack,
view.engineView,
binding.engineView,
customTabSessionId
),
owner = this,
@ -726,17 +718,17 @@ abstract class BaseBrowserFragment :
.collect { tab -> pipModeChanged(tab) }
}
view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false)
if (view.swipeRefresh.isEnabled) {
if (binding.swipeRefresh.isEnabled) {
val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.primaryText, context)
view.swipeRefresh.setColorSchemeColors(primaryTextColor)
binding.swipeRefresh.setColorSchemeColors(primaryTextColor)
swipeRefreshFeature.set(
feature = SwipeRefreshFeature(
requireComponents.core.store,
context.components.useCases.sessionUseCases.reload,
view.swipeRefresh,
binding.swipeRefresh,
customTabSessionId
),
owner = this,
@ -866,7 +858,6 @@ abstract class BaseBrowserFragment :
internal fun resumeDownloadDialogState(
sessionId: String?,
store: BrowserStore,
view: View,
context: Context,
toolbarHeight: Int
) {
@ -874,7 +865,7 @@ abstract class BaseBrowserFragment :
sharedViewModel.downloadDialogState[sessionId]
if (savedDownloadState == null || sessionId == null) {
view.viewDynamicDownloadDialog.visibility = View.GONE
binding.viewDynamicDownloadDialog.root.visibility = View.GONE
return
}
@ -892,14 +883,14 @@ abstract class BaseBrowserFragment :
{ sharedViewModel.downloadDialogState.remove(sessionId) }
DynamicDownloadDialog(
container = view.browserLayout,
context = context,
downloadState = savedDownloadState.first,
didFail = savedDownloadState.second,
tryAgain = onTryAgain,
onCannotOpenFile = {
showCannotOpenFileError(view.browserLayout, context, it)
showCannotOpenFileError(binding.browserLayout, context, it)
},
view = view.viewDynamicDownloadDialog,
binding = binding.viewDynamicDownloadDialog,
toolbarHeight = toolbarHeight,
onDismiss = onDismiss
).show()
@ -1018,13 +1009,13 @@ abstract class BaseBrowserFragment :
}
if (browserInitialized) {
view?.let { view ->
view?.let {
fullScreenChanged(false)
browserToolbarView.expand()
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
val context = requireContext()
resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight)
resumeDownloadDialogState(selectedTab.id, context.components.core.store, context, toolbarHeight)
}
} else {
view?.let { view -> initializeUI(view) }
@ -1163,8 +1154,6 @@ abstract class BaseBrowserFragment :
sitePermissions: SitePermissions?
)
protected abstract fun navToTrackingProtectionPanel(tab: SessionState)
/**
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
*/
@ -1200,13 +1189,6 @@ abstract class BaseBrowserFragment :
}
}
private fun showTrackingProtectionPanel() {
val tab = getCurrentTab() ?: return
view?.let {
navToTrackingProtectionPanel(tab)
}
}
/**
* Set the activity normal/private theme to match the current session.
*/
@ -1245,9 +1227,9 @@ abstract class BaseBrowserFragment :
withContext(Main) {
requireComponents.analytics.metrics.track(Event.AddBookmark)
view?.let { view ->
view?.let {
FenixSnackbar.make(
view = view.browserLayout,
view = binding.browserLayout,
duration = FenixSnackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true
)
@ -1298,7 +1280,7 @@ abstract class BaseBrowserFragment :
// Close find in page bar if opened
findInPageIntegration.onBackPressed()
FenixSnackbar.make(
view = requireView().browserLayout,
view = binding.browserLayout,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = false
)
@ -1307,14 +1289,14 @@ abstract class BaseBrowserFragment :
activity?.enterToImmersiveMode()
browserToolbarView.collapse()
browserToolbarView.view.isVisible = false
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
val browserEngine = binding.swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
browserEngine.bottomMargin = 0
browserEngine.topMargin = 0
swipeRefresh.translationY = 0f
binding.swipeRefresh.translationY = 0f
engineView.setDynamicToolbarMaxHeight(0)
binding.engineView.setDynamicToolbarMaxHeight(0)
// Without this, fullscreen has a margin at the top.
engineView.setVerticalClipping(0)
binding.engineView.setVerticalClipping(0)
requireComponents.analytics.metrics.track(Event.MediaFullscreenState)
} else {
@ -1330,7 +1312,7 @@ abstract class BaseBrowserFragment :
}
}
activity?.swipeRefresh?.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen)
binding.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen)
}
/*
@ -1348,6 +1330,7 @@ abstract class BaseBrowserFragment :
requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this)
_browserToolbarView = null
_browserToolbarInteractor = null
_binding = null
}
override fun onAttach(context: Context) {
@ -1424,13 +1407,13 @@ abstract class BaseBrowserFragment :
* Convenience method for replacing EngineView (id/engineView) in unit tests.
*/
@VisibleForTesting
internal fun getEngineView() = engineView
internal fun getEngineView() = binding.engineView
/**
* Convenience method for replacing SwipeRefreshLayout (id/swipeRefresh) in unit tests.
*/
@VisibleForTesting
internal fun getSwipeRefreshLayout() = swipeRefresh
internal fun getSwipeRefreshLayout() = binding.swipeRefresh
@VisibleForTesting
internal fun shouldShowCompletedDownloadDialog(

@ -13,8 +13,6 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
@ -33,16 +31,15 @@ import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
/**
* Fragment used for browsing the web within the main app.
@ -53,8 +50,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val trackingProtectionOverlayObserver =
ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
private var readerModeAvailable = false
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
@ -67,11 +62,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
val components = context.components
if (context.settings().isSwipeToolbarToSwitchTabsEnabled) {
gestureLayout.addGestureListener(
binding.gestureLayout.addGestureListener(
ToolbarGestureHandler(
activity = requireActivity(),
contentLayout = browserLayout,
tabPreview = tabPreview,
contentLayout = binding.browserLayout,
tabPreview = binding.tabPreview,
toolbarLayout = browserToolbarView.view,
store = components.core.store,
selectTabUseCase = components.useCases.tabsUseCases.selectTab
@ -82,10 +77,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
if (FeatureFlags.showHomeButtonFeature) {
val homeAction = BrowserToolbar.Button(
imageDrawable = AppCompatResources.getDrawable(
requireContext(),
context,
R.drawable.mozac_ic_home
)!!,
contentDescription = requireContext().getString(R.string.browser_toolbar_home),
contentDescription = context.getString(R.string.browser_toolbar_home),
iconTintColorResource = ThemeManager.resolveAttribute(R.attr.primaryText, context),
listener = browserToolbarInteractor::onHomeButtonClicked
)
@ -93,19 +88,100 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
browserToolbarView.view.addNavigationAction(homeAction)
}
if (resources.getBoolean(R.bool.tablet)) {
val enableTint = ThemeManager.resolveAttribute(R.attr.primaryText, context)
val disableTint = ThemeManager.resolveAttribute(R.attr.disabled, context)
val backAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_back
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_back),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoBack ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = true)
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Back(viewHistory = false)
)
}
)
browserToolbarView.view.addNavigationAction(backAction)
val forwardAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_forward
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_forward),
primaryImageTintResource = enableTint,
isInPrimaryState = { getCurrentTab()?.content?.canGoForward ?: false },
secondaryImageTintResource = disableTint,
disableInSecondaryState = true,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = true)
)
},
listener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Forward(viewHistory = false)
)
}
)
browserToolbarView.view.addNavigationAction(forwardAction)
val refreshAction = BrowserToolbar.TwoStateButton(
primaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_refresh
)!!,
primaryContentDescription = context.getString(R.string.browser_menu_refresh),
primaryImageTintResource = enableTint,
isInPrimaryState = {
getCurrentTab()?.content?.loading == false
},
secondaryImage = AppCompatResources.getDrawable(
context,
R.drawable.mozac_ic_stop
)!!,
secondaryContentDescription = context.getString(R.string.browser_menu_stop),
disableInSecondaryState = false,
longClickListener = {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = true)
)
},
listener = {
if (getCurrentTab()?.content?.loading == true) {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(ToolbarMenu.Item.Stop)
} else {
browserToolbarInteractor.onBrowserToolbarMenuItemTapped(
ToolbarMenu.Item.Reload(bypassCache = false)
)
}
}
)
browserToolbarView.view.addNavigationAction(refreshAction)
}
val readerModeAction =
BrowserToolbar.ToggleButton(
image = AppCompatResources.getDrawable(
requireContext(),
context,
R.drawable.ic_readermode
)!!,
imageSelected =
AppCompatResources.getDrawable(
requireContext(),
context,
R.drawable.ic_readermode_selected
)!!,
contentDescription = requireContext().getString(R.string.browser_menu_read),
contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close),
contentDescription = context.getString(R.string.browser_menu_read),
contentDescriptionSelected = context.getString(R.string.browser_menu_read_close),
visible = {
readerModeAvailable
},
@ -118,7 +194,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
browserToolbarView.view.addPageAction(readerModeAction)
thumbnailsFeature.set(
feature = BrowserThumbnails(context, view.engineView, components.core.store),
feature = BrowserThumbnails(context, binding.engineView, components.core.store),
owner = this,
view = view
)
@ -129,7 +205,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
context,
components.core.engine,
components.core.store,
view.readerViewControlsBar
binding.readerViewControlsBar
) { available, active ->
if (available) {
components.analytics.metrics.track(Event.ReaderModeAvailable)
@ -162,27 +238,13 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
navController = findNavController(),
settings = context.settings(),
appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserLayout as ViewGroup,
container = binding.browserLayout as ViewGroup,
shouldScrollWithTopToolbar = !context.settings().shouldUseBottomToolbar
),
owner = this,
view = view
)
}
if (context.settings().shouldShowTrackingProtectionCfr) {
trackingProtectionOverlayObserver.set(
feature = TrackingProtectionOverlay(
context = context,
store = context.components.core.store,
lifecycleOwner = viewLifecycleOwner,
settings = context.settings(),
metrics = context.components.analytics.metrics,
getToolbar = { browserToolbarView.view }
),
owner = this,
view = view
)
}
}
override fun onStart() {
@ -208,9 +270,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
override fun onStop() {
super.onStop()
updateLastBrowseActivity()
if (requireContext().settings().historyMetadataFeature) {
updateHistoryMetadata()
}
updateHistoryMetadata()
pwaOnboardingObserver?.stop()
}
@ -241,33 +301,22 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
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
)
nav(R.id.browserFragment, directions)
}
override fun navToTrackingProtectionPanel(tab: SessionState) {
val navController = findNavController()
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
runIfFragmentIsAttached {
val isEnabled = tab.trackingProtection.enabled && !contains
val isTrackingProtectionEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = tab.id,
url = tab.content.url,
trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity()
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights,
isTrackingProtectionEnabled = isTrackingProtectionEnabled
)
navController.navigateSafe(R.id.browserFragment, directions)
nav(R.id.browserFragment, directions)
}
}
}
@ -302,7 +351,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
}
FenixSnackbar.make(
view = view.browserLayout,
view = binding.browserLayout,
duration = Snackbar.LENGTH_SHORT,
isDisplayedWithBrowserToolbar = true
)

@ -13,11 +13,10 @@ import android.widget.FrameLayout
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams
import kotlinx.android.synthetic.main.tab_preview.view.*
import kotlinx.android.synthetic.main.tabs_tray_tab_counter.view.*
import mozilla.components.browser.thumbnails.loader.ThumbnailLoader
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.TabPreviewBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
@ -29,32 +28,30 @@ class TabPreview @JvmOverloads constructor(
defStyle: Int = 0
) : FrameLayout(context, attrs, defStyle) {
private val binding = TabPreviewBinding.inflate(LayoutInflater.from(context), this)
private val thumbnailLoader = ThumbnailLoader(context.components.core.thumbnailStorage)
init {
val inflater = LayoutInflater.from(context)
inflater.inflate(R.layout.tab_preview, this, true)
if (!context.settings().shouldUseBottomToolbar) {
fakeToolbar.updateLayoutParams<LayoutParams> {
binding.fakeToolbar.updateLayoutParams<LayoutParams> {
gravity = Gravity.TOP
}
fakeToolbar.background = AppCompatResources.getDrawable(
binding.fakeToolbar.background = AppCompatResources.getDrawable(
context,
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context)
)
}
// Change view properties to avoid confusing the UI tests
tab_button.counter_box.id = View.NO_ID
tab_button.counter_text.id = View.NO_ID
binding.tabButton.findViewById<View>(R.id.counter_box).id = View.NO_ID
binding.tabButton.findViewById<View>(R.id.counter_text).id = View.NO_ID
}
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
previewThumbnail.translationY = if (!context.settings().shouldUseBottomToolbar) {
fakeToolbar.height.toFloat()
binding.previewThumbnail.translationY = if (!context.settings().shouldUseBottomToolbar) {
binding.fakeToolbar.height.toFloat()
} else {
0f
}
@ -62,6 +59,7 @@ class TabPreview @JvmOverloads constructor(
fun loadPreviewThumbnail(thumbnailId: String) {
doOnNextLayout {
val previewThumbnail = binding.previewThumbnail
val thumbnailSize = max(previewThumbnail.height, previewThumbnail.width)
thumbnailLoader.loadIntoView(
previewThumbnail,

@ -34,7 +34,7 @@ class DynamicInfoBanner(
super.showBanner()
if (shouldScrollWithTopToolbar) {
(bannerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior = DynamicInfoBannerBehavior(
(binding.root.layoutParams as CoordinatorLayout.LayoutParams).behavior = DynamicInfoBannerBehavior(
context, null
)
}

@ -10,8 +10,7 @@ import android.view.LayoutInflater
import android.view.View.GONE
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import kotlinx.android.synthetic.main.info_banner.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.InfoBannerBinding
import org.mozilla.fenix.ext.settings
/**
@ -40,27 +39,26 @@ open class InfoBanner(
) {
@SuppressLint("InflateParams")
@VisibleForTesting
internal val bannerLayout = LayoutInflater.from(context)
.inflate(R.layout.info_banner, null)
internal val binding = InfoBannerBinding.inflate(LayoutInflater.from(context), container, false)
internal open fun showBanner() {
bannerLayout.banner_info_message.text = message
bannerLayout.dismiss.text = dismissText
binding.bannerInfoMessage.text = message
binding.dismiss.text = dismissText
if (actionText.isNullOrEmpty()) {
bannerLayout.action.visibility = GONE
binding.action.visibility = GONE
} else {
bannerLayout.action.text = actionText
binding.action.text = actionText
}
container.addView(bannerLayout)
container.addView(binding.root)
bannerLayout.dismiss.setOnClickListener {
binding.dismiss.setOnClickListener {
dismissAction?.invoke()
if (dismissByHiding) { bannerLayout.visibility = GONE } else { dismiss() }
if (dismissByHiding) { binding.root.visibility = GONE } else { dismiss() }
}
bannerLayout.action.setOnClickListener {
binding.action.setOnClickListener {
actionToPerform?.invoke()
}
@ -68,6 +66,6 @@ open class InfoBanner(
}
internal fun dismiss() {
container.removeView(bannerLayout)
container.removeView(binding.root)
}
}

@ -12,11 +12,11 @@ import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_create_collection.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.databinding.FragmentCreateCollectionBinding
import org.mozilla.fenix.ext.requireComponents
@ExperimentalCoroutinesApi
@ -25,6 +25,9 @@ class CollectionCreationFragment : DialogFragment() {
private lateinit var collectionCreationStore: CollectionCreationStore
private lateinit var collectionCreationInteractor: CollectionCreationInteractor
private var _binding: FragmentCreateCollectionBinding? = null
private val binding get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
isCancelable = false
@ -35,8 +38,8 @@ class CollectionCreationFragment : DialogFragment() {
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_create_collection, container, false)
): View {
_binding = FragmentCreateCollectionBinding.inflate(inflater, container, false)
val args: CollectionCreationFragmentArgs by navArgs()
collectionCreationStore = StoreProvider.get(this) {
@ -63,11 +66,11 @@ class CollectionCreationFragment : DialogFragment() {
)
)
collectionCreationView = CollectionCreationView(
view.createCollectionWrapper,
binding.createCollectionWrapper,
collectionCreationInteractor
)
return view
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -78,6 +81,11 @@ class CollectionCreationFragment : DialogFragment() {
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onResume() {
super.onResume()
collectionCreationView.onResumed()

@ -5,14 +5,13 @@
package org.mozilla.fenix.collections
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.collection_tab_list_row.*
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.CollectionTabListRowBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.home.Tab
@ -26,11 +25,16 @@ class CollectionCreationTabListAdapter(
private var selectedTabs: MutableSet<Tab> = mutableSetOf()
private var hideCheckboxes = false
private lateinit var binding: CollectionTabListRowBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(TabViewHolder.LAYOUT_ID, parent, false)
binding = CollectionTabListRowBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return TabViewHolder(view)
return TabViewHolder(binding)
}
override fun onBindViewHolder(holder: TabViewHolder, position: Int, payloads: MutableList<Any>) {
@ -41,11 +45,11 @@ class CollectionCreationTabListAdapter(
is CheckChanged -> {
val checkChanged = payloads[0] as CheckChanged
if (checkChanged.shouldBeChecked) {
holder.tab_selected_checkbox.isChecked = true
binding.tabSelectedCheckbox.isChecked = true
} else if (checkChanged.shouldBeUnchecked) {
holder.tab_selected_checkbox.isChecked = false
binding.tabSelectedCheckbox.isChecked = false
}
holder.tab_selected_checkbox.isGone = checkChanged.shouldHideCheckBox
binding.tabSelectedCheckbox.isGone = checkChanged.shouldHideCheckBox
}
}
}
@ -54,7 +58,7 @@ class CollectionCreationTabListAdapter(
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
val tab = tabs[position]
val isSelected = selectedTabs.contains(tab)
holder.tab_selected_checkbox.setOnCheckedChangeListener { _, isChecked ->
binding.tabSelectedCheckbox.setOnCheckedChangeListener { _, isChecked ->
if (isChecked) {
selectedTabs.add(tab)
interactor.addTabToSelection(tab)
@ -88,24 +92,24 @@ class CollectionCreationTabListAdapter(
}
}
class TabViewHolder(view: View) : ViewHolder(view) {
class TabViewHolder(private val binding: CollectionTabListRowBinding) : ViewHolder(binding.root) {
init {
collection_item_tab.setOnClickListener {
tab_selected_checkbox.isChecked = !tab_selected_checkbox.isChecked
binding.collectionItemTab.setOnClickListener {
binding.tabSelectedCheckbox.isChecked = !binding.tabSelectedCheckbox.isChecked
}
}
fun bind(tab: Tab, isSelected: Boolean, shouldHideCheckBox: Boolean) {
hostname.text = tab.hostname
tab_title.text = tab.title
tab_selected_checkbox.isInvisible = shouldHideCheckBox
binding.hostname.text = tab.hostname
binding.tabTitle.text = tab.title
binding.tabSelectedCheckbox.isInvisible = shouldHideCheckBox
itemView.isClickable = !shouldHideCheckBox
if (tab_selected_checkbox.isChecked != isSelected) {
tab_selected_checkbox.isChecked = isSelected
if (binding.tabSelectedCheckbox.isChecked != isSelected) {
binding.tabSelectedCheckbox.isChecked = isSelected
}
itemView.context.components.core.icons.loadIntoView(favicon_image, tab.url)
itemView.context.components.core.icons.loadIntoView(binding.faviconImage, tab.url)
}
companion object {

@ -9,7 +9,6 @@ import android.os.Looper
import android.text.InputFilter
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import androidx.constraintlayout.widget.ConstraintSet
@ -18,31 +17,33 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.transition.AutoTransition
import androidx.transition.Transition
import androidx.transition.TransitionManager
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_collection_creation.*
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.databinding.ComponentCollectionCreationBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.Tab
class CollectionCreationView(
container: ViewGroup,
private val container: ViewGroup,
private val interactor: CollectionCreationInteractor
) : LayoutContainer {
) {
override val containerView: View = LayoutInflater.from(container.context)
.inflate(R.layout.component_collection_creation, container, true)
private val binding = ComponentCollectionCreationBinding.inflate(
LayoutInflater.from(container.context),
container,
true
)
private val bottomBarView = CollectionCreationBottomBarView(
interactor = interactor,
layout = bottom_button_bar_layout,
iconButton = bottom_bar_icon_button,
textView = bottom_bar_text,
saveButton = save_button
layout = binding.bottomButtonBarLayout,
iconButton = binding.bottomBarIconButton,
textView = binding.bottomBarText,
saveButton = binding.saveButton
)
private val collectionCreationTabListAdapter = CollectionCreationTabListAdapter(interactor)
private val collectionSaveListAdapter = SaveCollectionListAdapter(interactor)
@ -58,10 +59,10 @@ class CollectionCreationView(
init {
transition.duration = TRANSITION_DURATION
transition.excludeTarget(back_button, true)
transition.excludeTarget(binding.backButton, true)
name_collection_edittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH)
name_collection_edittext.setOnEditorActionListener { view, actionId, _ ->
binding.nameCollectionEdittext.filters += InputFilter.LengthFilter(COLLECTION_NAME_MAX_LENGTH)
binding.nameCollectionEdittext.setOnEditorActionListener { view, actionId, _ ->
val text = view.text.toString()
if (actionId == EditorInfo.IME_ACTION_DONE && text.isNotBlank()) {
when (step) {
@ -75,15 +76,15 @@ class CollectionCreationView(
false
}
tab_list.run {
binding.tabList.run {
adapter = collectionCreationTabListAdapter
itemAnimator = null
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
layoutManager = LinearLayoutManager(container.context, RecyclerView.VERTICAL, true)
}
collections_list.run {
binding.collectionsList.run {
adapter = collectionSaveListAdapter
layoutManager = LinearLayoutManager(containerView.context, RecyclerView.VERTICAL, true)
layoutManager = LinearLayoutManager(container.context, RecyclerView.VERTICAL, true)
}
}
@ -109,18 +110,18 @@ class CollectionCreationView(
}
private fun updateForSelectTabs(state: CollectionCreationState) {
containerView.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened)
container.context.components.analytics.metrics.track(Event.CollectionTabSelectOpened)
tab_list.isClickable = true
binding.tabList.isClickable = true
back_button.apply {
binding.backButton.apply {
text = context.getString(R.string.create_collection_select_tabs)
setOnClickListener {
interactor.onBackPressed(SaveCollectionStep.SelectTabs)
}
}
select_all_button.apply {
binding.selectAllButton.apply {
val allSelected = state.selectedTabs.size == state.tabs.size
text =
if (allSelected) context.getString(R.string.create_collection_deselect_all)
@ -132,43 +133,43 @@ class CollectionCreationView(
}
selectTabsConstraints.clone(
containerView.context,
container.context,
R.layout.component_collection_creation
)
collectionCreationTabListAdapter.updateData(state.tabs, state.selectedTabs)
selectTabsConstraints.applyTo(collection_constraint_layout)
selectTabsConstraints.applyTo(binding.collectionConstraintLayout)
}
private fun updateForSelectCollection() {
tab_list.isClickable = false
binding.tabList.isClickable = false
selectCollectionConstraints.clone(
containerView.context,
container.context,
R.layout.component_collection_creation_select_collection
)
selectCollectionConstraints.applyTo(collection_constraint_layout)
selectCollectionConstraints.applyTo(binding.collectionConstraintLayout)
back_button.apply {
binding.backButton.apply {
text = context.getString(R.string.create_collection_select_collection)
setOnClickListener {
interactor.onBackPressed(SaveCollectionStep.SelectCollection)
}
}
TransitionManager.beginDelayedTransition(collection_constraint_layout, transition)
TransitionManager.beginDelayedTransition(binding.collectionConstraintLayout, transition)
}
private fun updateForNameCollection(state: CollectionCreationState) {
tab_list.isClickable = false
binding.tabList.isClickable = false
nameCollectionConstraints.clone(
containerView.context,
container.context,
R.layout.component_collection_creation_name_collection
)
nameCollectionConstraints.applyTo(collection_constraint_layout)
nameCollectionConstraints.applyTo(binding.collectionConstraintLayout)
collectionCreationTabListAdapter.updateData(state.selectedTabs.toList(), state.selectedTabs, true)
back_button.apply {
binding.backButton.apply {
text = context.getString(R.string.create_collection_name_collection)
setOnClickListener {
name_collection_edittext.hideKeyboard()
binding.nameCollectionEdittext.hideKeyboard()
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(
{
@ -179,22 +180,22 @@ class CollectionCreationView(
}
}
name_collection_edittext.showKeyboard()
binding.nameCollectionEdittext.showKeyboard()
name_collection_edittext.setText(
containerView.context.getString(
binding.nameCollectionEdittext.setText(
container.context.getString(
R.string.create_collection_default_name,
state.defaultCollectionNumber
)
)
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
binding.nameCollectionEdittext.setSelection(0, binding.nameCollectionEdittext.text.length)
}
private fun updateForRenameCollection(state: CollectionCreationState) {
tab_list.isClickable = false
binding.tabList.isClickable = false
state.selectedTabCollection?.let { tabCollection ->
val publicSuffixList = containerView.context.components.publicSuffixList
val publicSuffixList = container.context.components.publicSuffixList
tabCollection.tabs.map { tab ->
Tab(
sessionId = tab.id.toString(),
@ -207,17 +208,17 @@ class CollectionCreationView(
}
}
nameCollectionConstraints.clone(
containerView.context,
container.context,
R.layout.component_collection_creation_name_collection
)
nameCollectionConstraints.applyTo(collection_constraint_layout)
name_collection_edittext.setText(state.selectedTabCollection?.title)
name_collection_edittext.setSelection(0, name_collection_edittext.text.length)
nameCollectionConstraints.applyTo(binding.collectionConstraintLayout)
binding.nameCollectionEdittext.setText(state.selectedTabCollection?.title)
binding.nameCollectionEdittext.setSelection(0, binding.nameCollectionEdittext.text.length)
back_button.apply {
binding.backButton.apply {
text = context.getString(R.string.collection_rename)
setOnClickListener {
name_collection_edittext.hideKeyboard()
binding.nameCollectionEdittext.hideKeyboard()
val handler = Handler(Looper.getMainLooper())
handler.postDelayed(
{
@ -231,7 +232,7 @@ class CollectionCreationView(
override fun onTransitionStart(transition: Transition) { /* noop */ }
override fun onTransitionEnd(transition: Transition) {
name_collection_edittext.showKeyboard()
binding.nameCollectionEdittext.showKeyboard()
transition.removeListener(this)
}
@ -239,12 +240,12 @@ class CollectionCreationView(
override fun onTransitionPause(transition: Transition) { /* noop */ }
override fun onTransitionResume(transition: Transition) { /* noop */ }
})
TransitionManager.beginDelayedTransition(collection_constraint_layout, transition)
TransitionManager.beginDelayedTransition(binding.collectionConstraintLayout, transition)
}
fun onResumed() {
if (step == SaveCollectionStep.NameCollection || step == SaveCollectionStep.RenameCollection) {
name_collection_edittext.showKeyboard()
binding.nameCollectionEdittext.showKeyboard()
}
}

@ -5,15 +5,14 @@
package org.mozilla.fenix.collections
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.graphics.BlendModeColorFilterCompat.createBlendModeColorFilterCompat
import androidx.core.graphics.BlendModeCompat.SRC_IN
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.collections_list_item.*
import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.R
import org.mozilla.fenix.components.description
import org.mozilla.fenix.databinding.CollectionsListItemBinding
import org.mozilla.fenix.ext.getIconColor
import org.mozilla.fenix.home.Tab
import org.mozilla.fenix.utils.view.ViewHolder
@ -26,10 +25,13 @@ class SaveCollectionListAdapter(
private var selectedTabs: Set<Tab> = setOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CollectionViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(CollectionViewHolder.LAYOUT_ID, parent, false)
val binding = CollectionsListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return CollectionViewHolder(view)
return CollectionViewHolder(binding)
}
override fun onBindViewHolder(holder: CollectionViewHolder, position: Int) {
@ -49,12 +51,12 @@ class SaveCollectionListAdapter(
}
}
class CollectionViewHolder(view: View) : ViewHolder(view) {
class CollectionViewHolder(private val binding: CollectionsListItemBinding) : ViewHolder(binding.root) {
fun bind(collection: TabCollection) {
collection_item.text = collection.title
collection_description.text = collection.description(itemView.context)
collection_icon.colorFilter =
binding.collectionItem.text = collection.title
binding.collectionDescription.text = collection.description(itemView.context)
binding.collectionIcon.colorFilter =
createBlendModeColorFilterCompat(collection.getIconColor(itemView.context), SRC_IN)
}

@ -22,6 +22,7 @@ import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.NimbusFeatures
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
@ -103,6 +104,10 @@ class Analytics(
val experiments: NimbusApi by lazyMonitored {
createNimbus(context, BuildConfig.NIMBUS_ENDPOINT)
}
val features: NimbusFeatures by lazyMonitored {
NimbusFeatures(context)
}
}
fun isSentryEnabled() = !BuildConfig.SENTRY_TOKEN.isNullOrEmpty()

@ -59,6 +59,9 @@ import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
import mozilla.components.service.location.LocationService
import mozilla.components.service.location.MozillaLocationService
import mozilla.components.service.pocket.Frequency
import mozilla.components.service.pocket.PocketStoriesConfig
import mozilla.components.service.pocket.PocketStoriesService
import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage
import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.locale.LocaleManager
@ -84,6 +87,8 @@ import org.mozilla.fenix.telemetry.TelemetryMiddleware
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay
import org.mozilla.geckoview.GeckoRuntime
import java.lang.IllegalStateException
import java.util.concurrent.TimeUnit
/**
* Component group for all core browser functionality.
@ -208,13 +213,10 @@ class Core(
RecordingDevicesMiddleware(context),
PromptMiddleware(),
AdsTelemetryMiddleware(adsTelemetry),
LastMediaAccessMiddleware()
LastMediaAccessMiddleware(),
HistoryMetadataMiddleware(historyMetadataService)
)
if (context.settings().historyMetadataFeature) {
middlewareList += HistoryMetadataMiddleware(historyMetadataService)
}
BrowserStore(
middleware = middlewareList + EngineMiddleware.create(engine)
).apply {
@ -252,9 +254,7 @@ class Core(
* The [HistoryMetadataService] is used to record history metadata.
*/
val historyMetadataService: HistoryMetadataService by lazyMonitored {
DefaultHistoryMetadataService(storage = historyStorage).apply {
cleanup(System.currentTimeMillis() - HISTORY_METADATA_MAX_AGE_IN_MS)
}
DefaultHistoryMetadataService(storage = historyStorage)
}
/**
@ -322,6 +322,12 @@ class Core(
val pinnedSiteStorage by lazyMonitored { PinnedSiteStorage(context) }
@Suppress("MagicNumber")
val pocketStoriesConfig by lazyMonitored {
PocketStoriesConfig(client, Frequency(4, TimeUnit.HOURS))
}
val pocketStoriesService by lazyMonitored { PocketStoriesService(context, pocketStoriesConfig) }
val topSitesStorage by lazyMonitored {
val defaultTopSites = mutableListOf<Pair<String, String>>()
@ -348,6 +354,13 @@ class Core(
SupportUtils.PDD_URL
)
)
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_tc),
SupportUtils.TC_URL
)
)
} else {
defaultTopSites.add(
Pair(
@ -444,6 +457,6 @@ class Core(
private const val KEY_STORAGE_NAME = "core_prefs"
private const val PASSWORDS_KEY = "passwords"
private const val RECENTLY_CLOSED_MAX = 10
private const val HISTORY_METADATA_MAX_AGE_IN_MS = 14 * 24 * 60 * 60 * 1000 // 14 days
const val HISTORY_METADATA_MAX_AGE_IN_MS = 14 * 24 * 60 * 60 * 1000 // 14 days
}
}

@ -18,8 +18,8 @@ import androidx.core.widget.TextViewCompat
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.ContentViewCallback
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fenix_snackbar.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.databinding.FenixSnackbarBinding
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Mockable
@ -27,20 +27,20 @@ import org.mozilla.fenix.utils.Mockable
@Mockable
class FenixSnackbar private constructor(
parent: ViewGroup,
content: View,
private val binding: FenixSnackbarBinding,
contentViewCallback: FenixSnackbarCallback,
isError: Boolean
) : BaseTransientBottomBar<FenixSnackbar>(parent, content, contentViewCallback) {
) : BaseTransientBottomBar<FenixSnackbar>(parent, binding.root, contentViewCallback) {
init {
view.setBackgroundColor(Color.TRANSPARENT)
setAppropriateBackground(isError)
content.snackbar_btn.increaseTapArea(actionButtonIncreaseDps)
binding.snackbarBtn.increaseTapArea(actionButtonIncreaseDps)
TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
content.snackbar_text,
binding.snackbarText,
minTextSize,
maxTextSize,
stepGranularity,
@ -49,7 +49,7 @@ class FenixSnackbar private constructor(
}
fun setAppropriateBackground(isError: Boolean) {
view.snackbar_layout.background = if (isError) {
binding.snackbarLayout.background = if (isError) {
AppCompatResources.getDrawable(context, R.drawable.fenix_snackbar_error_background)
} else {
AppCompatResources.getDrawable(context, R.drawable.fenix_snackbar_background)
@ -57,7 +57,7 @@ class FenixSnackbar private constructor(
}
fun setText(text: String) = this.apply {
view.snackbar_text.text = text
binding.snackbarText.text = text
}
fun setLength(duration: Int) = this.apply {
@ -65,7 +65,7 @@ class FenixSnackbar private constructor(
}
fun setAction(text: String, action: () -> Unit) = this.apply {
view.snackbar_btn.apply {
binding.snackbarBtn.apply {
setText(text)
visibility = View.VISIBLE
setOnClickListener {
@ -110,7 +110,7 @@ class FenixSnackbar private constructor(
}
val inflater = LayoutInflater.from(parent.context)
val content = inflater.inflate(R.layout.fenix_snackbar, parent, false)
val binding = FenixSnackbarBinding.inflate(inflater, parent, false)
val durationOrAccessibleDuration =
if (parent.context.settings().accessibilityServicesEnabled) {
@ -119,12 +119,12 @@ class FenixSnackbar private constructor(
duration
}
val callback = FenixSnackbarCallback(content)
val callback = FenixSnackbarCallback(binding.root)
val shouldUseBottomToolbar = view.context.settings().shouldUseBottomToolbar
val toolbarHeight = view.resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
val dynamicToolbarEnabled = view.context.settings().isDynamicToolbarEnabled
return FenixSnackbar(parent, content, callback, isError).also {
return FenixSnackbar(parent, binding, callback, isError).also {
it.duration = durationOrAccessibleDuration
it.view.updatePadding(

@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage
@ -68,6 +69,15 @@ class TabCollectionStorage(
}
}
suspend fun createCollection(tabCollection: TabCollection): Long? {
return withContext(ioScope.coroutineContext) {
val sessions = tabCollection.tabs.map { createTab(url = it.url, title = it.title) }
val id = collectionStorage.createCollection(tabCollection.title, sessions)
notifyObservers { onCollectionCreated(tabCollection.title, sessions, id) }
id
}
}
suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List<TabSessionState>): Long? {
return withContext(ioScope.coroutineContext) {
val id = collectionStorage.addTabsToCollection(tabCollection, sessions)

@ -4,51 +4,195 @@
package org.mozilla.fenix.components.history
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import mozilla.components.concept.storage.VisitInfo
import mozilla.components.concept.storage.VisitType
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.toHistoryMetadata
import org.mozilla.fenix.perf.runBlockingIncrement
import kotlin.math.abs
private const val BUFFER_TIME = 15000 /* 15 seconds in ms */
/**
* An Interface for providing a paginated list of [VisitInfo]
* An Interface for providing a paginated list of [History].
*/
interface PagedHistoryProvider {
/**
* Gets a list of [VisitInfo]
* Gets a list of [History].
*
* @param offset How much to offset the list by
* @param numberOfItems How many items to fetch
* @param onComplete A callback that returns the list of [VisitInfo]
* @param onComplete A callback that returns the list of [History]
*/
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<VisitInfo>) -> Unit)
fun getHistory(offset: Long, numberOfItems: Long, onComplete: (List<History>) -> Unit)
}
// A PagedList DataSource runs on a background thread automatically.
// If we run this in our own coroutineScope it breaks the PagedList
fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider {
return object : PagedHistoryProvider {
override fun getHistory(
offset: Long,
numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit
) {
runBlockingIncrement {
val history = getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
/**
* @param historyStorage
*/
class DefaultPagedHistoryProvider(
private val historyStorage: PlacesHistoryStorage,
private val showHistorySearchGroups: Boolean = FeatureFlags.showHistorySearchGroups,
) : PagedHistoryProvider {
@Volatile private var historyGroups: List<History.Group>? = null
@Suppress("LongMethod")
override fun getHistory(
offset: Long,
numberOfItems: Long,
onComplete: (List<History>) -> Unit,
) {
// A PagedList DataSource runs on a background thread automatically.
// If we run this in our own coroutineScope it breaks the PagedList
runBlockingIncrement {
val history: List<History>
if (showHistorySearchGroups) {
// We need to refetch all the history metadata if the offset resets back at 0
// in the case of a pull to refresh.
if (historyGroups == null || offset == 0L) {
historyGroups = historyStorage.getHistoryMetadataSince(Long.MIN_VALUE)
.sortedByDescending { it.createdAt }
.filter { it.key.searchTerm != null }
.groupBy { it.key.searchTerm!! }
.map { (searchTerm, items) ->
History.Group(
id = items.first().createdAt.toInt(),
title = searchTerm,
visitedAt = items.first().createdAt,
items = items.map { it.toHistoryMetadata() }
)
}
}
history = getHistoryAndSearchGroups(offset, numberOfItems)
} else {
history = historyStorage
.getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
.mapIndexed(transformVisitInfoToHistoryItem(offset.toInt()))
}
onComplete(history)
}
}
/**
* Returns the [History.Regular] corresponding to the given [History.Metadata] item.
*
* @param historyMetadata The [History.Metadata] to match.
* @return the [History.Regular] corresponding to the given [History.Metadata] item or null.
*/
suspend fun getMatchingHistory(historyMetadata: History.Metadata): VisitInfo? {
val history = historyStorage.getDetailedVisits(
start = historyMetadata.visitedAt - BUFFER_TIME,
end = historyMetadata.visitedAt + BUFFER_TIME,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
return history
.filter { it.url == historyMetadata.url }
.minByOrNull { abs(historyMetadata.visitedAt - it.visitTime) }
}
/**
* Clears the history groups to refetch the most history metadata after any changes.
*/
fun clearHistoryGroups() {
historyGroups = null
}
@Suppress("MagicNumber")
private suspend fun getHistoryAndSearchGroups(
offset: Long,
numberOfItems: Long,
): List<History> {
val result = mutableListOf<History>()
val history: List<History.Regular> = historyStorage
.getVisitsPaginated(
offset,
numberOfItems,
excludeTypes = listOf(
VisitType.NOT_A_VISIT,
VisitType.DOWNLOAD,
VisitType.REDIRECT_TEMPORARY,
VisitType.RELOAD,
VisitType.EMBED,
VisitType.FRAMED_LINK,
VisitType.REDIRECT_PERMANENT
)
)
.mapIndexed(transformVisitInfoToHistoryItem(offset.toInt()))
// History metadata items are recorded after their associated visited info, we add an
// additional buffer time to the most recent visit to account for a history group
// appearing as the most recent item.
val visitedAtBuffer = if (offset == 0L) BUFFER_TIME else 0
onComplete(history)
// Get the history groups that fit within the range of visited times in the current history
// items.
val historyGroupsInOffset = if (history.isNotEmpty()) {
historyGroups?.filter {
history.last().visitedAt <= it.visitedAt - visitedAtBuffer &&
it.visitedAt - visitedAtBuffer <= (history.first().visitedAt + visitedAtBuffer)
} ?: emptyList()
} else {
emptyList()
}
val historyMetadata = historyGroupsInOffset.flatMap { it.items }
// Add all history items that are not in a group filtering out any matches with a history
// metadata item.
result.addAll(history.filter { item -> historyMetadata.find { it.url == item.url } == null })
// Filter history metadata items with no view time and dedupe by url.
// Note that distinctBy is sufficient here as it keeps the order of the source
// collection, and we're only sorting by visitedAt (=updatedAt) currently.
// If we needed the view time we'd have to aggregate it for entries with the same
// url, but we don't have a use case for this currently in the history view.
result.addAll(
historyGroupsInOffset.map { group ->
group.copy(items = group.items.distinctBy { it.url })
}
)
return result.sortedByDescending { it.visitedAt }
}
private fun transformVisitInfoToHistoryItem(offset: Int): (id: Int, visit: VisitInfo) -> History.Regular {
return { id, visit ->
val title = visit.title
?.takeIf(String::isNotEmpty)
?: visit.url.tryGetHostFromUrl()
History.Regular(
id = offset + id,
title = title,
url = visit.url,
visitedAt = visit.visitTime
)
}
}
}

@ -17,10 +17,14 @@ import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.GleanMetrics.Pocket
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites
import org.mozilla.fenix.GleanMetrics.TrackingProtection
@ -78,6 +82,9 @@ sealed class Event {
object HistoryOpenedInPrivateTabs : Event()
object HistoryItemRemoved : Event()
object HistoryAllItemsRemoved : Event()
data class HistoryRecentSearchesTapped(val source: String) : Event() {
override val extras = mapOf(History.recentSearchesTappedKeys.pageNumber to source)
}
object ReaderModeAvailable : Event()
object ReaderModeOpened : Event()
object ReaderModeClosed : Event()
@ -105,11 +112,14 @@ sealed class Event {
object NotificationMediaPause : Event()
object TopSiteOpenDefault : Event()
object TopSiteOpenGoogle : Event()
object TopSiteOpenBaidu : Event()
object TopSiteOpenFrecent : Event()
object TopSiteOpenPinned : Event()
object TopSiteOpenInNewTab : Event()
object TopSiteOpenInPrivateTab : Event()
object TopSiteRemoved : Event()
object GoogleTopSiteRemoved : Event()
object BaiduTopSiteRemoved : Event()
object TrackingProtectionTrackerList : Event()
object TrackingProtectionIconPressed : Event()
object TrackingProtectionSettingsPanel : Event()
@ -125,14 +135,43 @@ sealed class Event {
object WhatsNewTapped : Event()
object PocketTopSiteClicked : Event()
object PocketTopSiteRemoved : Event()
object PocketHomeRecsShown : Event()
object PocketHomeRecsDiscoverMoreClicked : Event()
object PocketHomeRecsLearnMoreClicked : Event()
data class PocketHomeRecsStoryClicked(
val timesShown: Long,
val storyPosition: Pair<Int, Int>,
) : Event() {
override val extras: Map<Pocket.homeRecsStoryClickedKeys, String>
get() = mapOf(
Pocket.homeRecsStoryClickedKeys.timesShown to timesShown.toString(),
Pocket.homeRecsStoryClickedKeys.position to "${storyPosition.first}x${storyPosition.second}"
)
}
data class PocketHomeRecsCategoryClicked(
val categoryname: String,
val previousSelectedCategoriesTotal: Int,
val isSelectedNextState: Boolean
) : Event() {
override val extras: Map<Pocket.homeRecsCategoryClickedKeys, String>
get() = mapOf(
Pocket.homeRecsCategoryClickedKeys.categoryName to categoryname,
Pocket.homeRecsCategoryClickedKeys.selectedTotal to previousSelectedCategoriesTotal.toString(),
Pocket.homeRecsCategoryClickedKeys.newState to when (isSelectedNextState) {
true -> "selected"
false -> "deselected"
}
)
}
object FennecToFenixMigrated : Event()
object AddonsOpenInSettings : Event()
object StudiesSettings : Event()
object VoiceSearchTapped : Event()
object SearchWidgetInstalled : Event()
object OnboardingAutoSignIn : Event()
object OnboardingManualSignIn : Event()
object OnboardingPrivacyNotice : Event()
object OnboardingPrivateBrowsing : Event()
object OnboardingFinish : Event()
object ChangedToDefaultBrowser : Event()
object DefaultBrowserNotifTapped : Event()
@ -142,16 +181,15 @@ sealed class Event {
object LoginDialogPromptSave : Event()
object LoginDialogPromptNeverSave : Event()
object ContextualHintETPDisplayed : Event()
object ContextualHintETPDismissed : Event()
object ContextualHintETPOutsideTap : Event()
object ContextualHintETPInsideTap : Event()
// Tab tray
object TabsTrayOpened : Event()
object TabsTrayClosed : Event()
object OpenedExistingTab : Event()
object ClosedExistingTab : Event()
data class OpenedExistingTab(val source: String) : Event() {
override val extras = mapOf(TabsTray.openedExistingTabKeys.source to source)
}
data class ClosedExistingTab(val source: String) : Event() {
override val extras = mapOf(TabsTray.closedExistingTabKeys.source to source)
}
object TabsTrayPrivateModeTapped : Event()
object TabsTrayNormalModeTapped : Event()
object TabsTraySyncedModeTapped : Event()
@ -161,6 +199,24 @@ sealed class Event {
object TabsTraySaveToCollectionPressed : Event()
object TabsTrayShareAllTabsPressed : Event()
object TabsTrayCloseAllTabsPressed : Event()
object TabsTrayRecentlyClosedPressed : Event()
object TabsTrayInactiveTabsExpanded : Event()
object TabsTrayInactiveTabsCollapsed : Event()
object TabsTrayAutoCloseDialogSeen : Event()
object TabsTrayAutoCloseDialogTurnOnClicked : Event()
object TabsTrayAutoCloseDialogDismissed : Event()
data class TabsTrayHasInactiveTabs(val count: Int) : Event() {
override val extras = mapOf(TabsTray.hasInactiveTabsKeys.inactiveTabsCount to count.toString())
}
object TabsTrayCloseAllInactiveTabs : Event()
data class TabsTrayCloseInactiveTab(val amountClosed: Int = 1) : Event()
object TabsTrayOpenInactiveTab : Event()
object InactiveTabsSurveyOpened : Event()
data class InactiveTabsOffSurvey(val feedback: String) : Event() {
override val extras: Map<Preferences.turnOffInactiveTabsSurveyKeys, String>
get() = mapOf(Preferences.turnOffInactiveTabsSurveyKeys.feedback to feedback.lowercase(Locale.ROOT))
}
object ProgressiveWebAppOpenFromHomescreenTap : Event()
object ProgressiveWebAppInstallAsShortcut : Event()
@ -198,6 +254,7 @@ sealed class Event {
// Home menu interaction
object HomeMenuSettingsItemClicked : Event()
object HomeScreenDisplayed : Event()
object HomeScreenCustomizedHomeClicked : Event()
// Browser Toolbar
object BrowserToolbarHomeButtonClicked : Event()
@ -210,6 +267,17 @@ sealed class Event {
object ShowAllRecentTabs : Event()
object OpenRecentTab : Event()
object OpenInProgressMediaTab : Event()
object RecentTabsSectionIsVisible : Event()
object RecentTabsSectionIsNotVisible : Event()
// Recent bookmarks
object BookmarkClicked : Event()
object ShowAllBookmarks : Event()
object RecentBookmarksShown : Event()
data class RecentBookmarkCount(val count: Int) : Event()
// Recently visited/Recent searches
object RecentSearchesGroupDeleted : Event()
// Android Autofill
object AndroidAutofillUnlockSuccessful : Event()
@ -221,6 +289,18 @@ sealed class Event {
object AndroidAutofillRequestWithLogins : Event()
object AndroidAutofillRequestWithoutLogins : Event()
// Credit cards
object CreditCardSaved : Event()
object CreditCardDeleted : Event()
object CreditCardModified : Event()
object CreditCardFormDetected : Event()
object CreditCardAutofilled : Event()
object CreditCardAutofillPromptShown : Event()
object CreditCardAutofillPromptExpanded : Event()
object CreditCardAutofillPromptDismissed : Event()
object CreditCardManagementAddTapped : Event()
object CreditCardManagementCardTapped : Event()
// Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -317,6 +397,31 @@ sealed class Event {
}
}
data class CustomizeHomePreferenceToggled(
val preferenceKey: String,
val enabled: Boolean,
val context: Context
) : Event() {
private val telemetryAllowMap = mapOf(
context.getString(R.string.pref_key_enable_top_frecent_sites) to "most_visited_sites",
context.getString(R.string.pref_key_recent_tabs) to "jump_back_in",
context.getString(R.string.pref_key_recent_bookmarks) to "recently_saved",
context.getString(R.string.pref_key_history_metadata_feature) to "recently_visited",
context.getString(R.string.pref_key_pocket_homescreen_recommendations) to "pocket",
)
override val extras: Map<Events.preferenceToggledKeys, String>
get() = mapOf(
Events.preferenceToggledKeys.preferenceKey to (telemetryAllowMap[preferenceKey] ?: ""),
Events.preferenceToggledKeys.enabled to enabled.toString()
)
init {
// If the event is not in the allow list, we don't want to track it
require(telemetryAllowMap.contains(preferenceKey))
}
}
data class AddonsOpenInToolbarMenu(val addonId: String) : Event() {
override val extras: Map<Addons.openAddonInToolbarMenuKeys, String>?
get() = hashMapOf(Addons.openAddonInToolbarMenuKeys.addonId to addonId)

@ -18,10 +18,11 @@ import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection
import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.CreditCards
import org.mozilla.fenix.GleanMetrics.CustomTab
import org.mozilla.fenix.GleanMetrics.CustomizeHome
import org.mozilla.fenix.GleanMetrics.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.ExperimentsDefaultBrowser
@ -36,8 +37,11 @@ import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.Onboarding
import org.mozilla.fenix.GleanMetrics.Pings
import org.mozilla.fenix.GleanMetrics.Pocket
import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.RecentBookmarks
import org.mozilla.fenix.GleanMetrics.RecentSearches
import org.mozilla.fenix.GleanMetrics.RecentTabs
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.SearchWidget
@ -280,6 +284,10 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.preferenceToggled.record(it) },
{ Events.preferenceToggledKeys.valueOf(it) }
)
is Event.CustomizeHomePreferenceToggled -> EventWrapper(
{ CustomizeHome.preferenceToggled.record(it) },
{ CustomizeHome.preferenceToggledKeys.valueOf(it) }
)
is Event.HistoryOpened -> EventWrapper<NoExtraKeys>(
{ History.opened.record(it) }
)
@ -307,6 +315,10 @@ private val Event.wrapper: EventWrapper<*>?
is Event.HistoryAllItemsRemoved -> EventWrapper<NoExtraKeys>(
{ History.removedAll.record(it) }
)
is Event.HistoryRecentSearchesTapped -> EventWrapper(
{ History.recentSearchesTapped.record(it) },
{ History.recentSearchesTappedKeys.valueOf(it) }
)
is Event.CollectionRenamed -> EventWrapper<NoExtraKeys>(
{ Collections.renamed.record(it) }
)
@ -455,6 +467,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TopSiteOpenGoogle -> EventWrapper<NoExtraKeys>(
{ TopSites.openGoogleSearchAttribution.record(it) }
)
is Event.TopSiteOpenBaidu -> EventWrapper<NoExtraKeys>(
{ TopSites.openBaiduSearchAttribution.record(it) }
)
is Event.TopSiteOpenFrecent -> EventWrapper<NoExtraKeys>(
{ TopSites.openFrecency.record(it) }
)
@ -470,6 +485,12 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TopSiteRemoved -> EventWrapper<NoExtraKeys>(
{ TopSites.remove.record(it) }
)
is Event.GoogleTopSiteRemoved -> EventWrapper<NoExtraKeys>(
{ TopSites.googleTopSiteRemoved.record(it) }
)
is Event.BaiduTopSiteRemoved -> EventWrapper<NoExtraKeys>(
{ TopSites.baiduTopSiteRemoved.record(it) }
)
is Event.TopSiteLongPress -> EventWrapper(
{ TopSites.longPress.record(it) },
{ TopSites.longPressKeys.valueOf(it) }
@ -484,6 +505,23 @@ private val Event.wrapper: EventWrapper<*>?
is Event.PocketTopSiteRemoved -> EventWrapper<NoExtraKeys>(
{ Pocket.pocketTopSiteRemoved.record(it) }
)
is Event.PocketHomeRecsShown -> EventWrapper<NoExtraKeys>(
{ Pocket.homeRecsShown.record(it) }
)
is Event.PocketHomeRecsLearnMoreClicked -> EventWrapper<NoExtraKeys>(
{ Pocket.homeRecsLearnMoreClicked.record(it) }
)
is Event.PocketHomeRecsDiscoverMoreClicked -> EventWrapper<NoExtraKeys>(
{ Pocket.homeRecsDiscoverClicked.record(it) }
)
is Event.PocketHomeRecsStoryClicked -> EventWrapper(
{ Pocket.homeRecsStoryClicked.record(it) },
{ Pocket.homeRecsStoryClickedKeys.valueOf(it) }
)
is Event.PocketHomeRecsCategoryClicked -> EventWrapper(
{ Pocket.homeRecsCategoryClicked.record(it) },
{ Pocket.homeRecsCategoryClickedKeys.valueOf(it) }
)
is Event.DarkThemeSelected -> EventWrapper(
{ AppTheme.darkThemeSelected.record(it) },
{ AppTheme.darkThemeSelectedKeys.valueOf(it) }
@ -491,6 +529,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.AddonsOpenInSettings -> EventWrapper<NoExtraKeys>(
{ Addons.openAddonsInSettings.record(it) }
)
is Event.StudiesSettings -> EventWrapper<NoExtraKeys>(
{ Preferences.studiesPreferenceEnabled.record(it) }
)
is Event.AddonsOpenInToolbarMenu -> EventWrapper(
{ Addons.openAddonInToolbarMenu.record(it) },
{ Addons.openAddonInToolbarMenuKeys.valueOf(it) }
@ -506,9 +547,6 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.tabCounterMenuAction.record(it) },
{ Events.tabCounterMenuActionKeys.valueOf(it) }
)
is Event.OnboardingPrivateBrowsing -> EventWrapper<NoExtraKeys>(
{ Onboarding.prefToggledPrivateBrowsing.record(it) }
)
is Event.OnboardingPrivacyNotice -> EventWrapper<NoExtraKeys>(
{ Onboarding.privacyNotice.record(it) }
)
@ -534,33 +572,19 @@ private val Event.wrapper: EventWrapper<*>?
{ Onboarding.prefToggledToolbarPositionKeys.valueOf(it) }
)
is Event.ContextualHintETPDisplayed -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.display.record(it) }
)
is Event.ContextualHintETPDismissed -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.dismiss.record(it) }
)
is Event.ContextualHintETPInsideTap -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.insideTap.record(it) }
)
is Event.ContextualHintETPOutsideTap -> EventWrapper<NoExtraKeys>(
{ ContextualHintTrackingProtection.outsideTap.record(it) }
)
is Event.TabsTrayOpened -> EventWrapper<NoExtraKeys>(
{ TabsTray.opened.record(it) }
)
is Event.TabsTrayClosed -> EventWrapper<NoExtraKeys>(
{ TabsTray.closed.record(it) }
)
is Event.OpenedExistingTab -> EventWrapper<NoExtraKeys>(
{ TabsTray.openedExistingTab.record(it) }
is Event.OpenedExistingTab -> EventWrapper(
{ TabsTray.openedExistingTab.record(it) },
{ TabsTray.openedExistingTabKeys.valueOf(it) }
)
is Event.ClosedExistingTab -> EventWrapper<NoExtraKeys>(
{ TabsTray.closedExistingTab.record(it) }
is Event.ClosedExistingTab -> EventWrapper(
{ TabsTray.closedExistingTab.record(it) },
{ TabsTray.closedExistingTabKeys.valueOf(it) }
)
is Event.TabsTrayPrivateModeTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.privateModeTapped.record(it) }
@ -589,6 +613,44 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ TabsTray.closeAllTabs.record(it) }
)
is Event.TabsTrayRecentlyClosedPressed -> EventWrapper<NoExtraKeys>(
{ TabsTray.inactiveTabsRecentlyClosed.record(it) }
)
is Event.TabsTrayInactiveTabsExpanded -> EventWrapper<NoExtraKeys>(
{ TabsTray.inactiveTabsExpanded.record(it) }
)
is Event.TabsTrayInactiveTabsCollapsed -> EventWrapper<NoExtraKeys>(
{ TabsTray.inactiveTabsCollapsed.record(it) }
)
is Event.TabsTrayAutoCloseDialogDismissed -> EventWrapper<NoExtraKeys>(
{ TabsTray.autoCloseDimissed.record(it) }
)
is Event.TabsTrayAutoCloseDialogSeen -> EventWrapper<NoExtraKeys>(
{ TabsTray.autoCloseSeen.record(it) }
)
is Event.TabsTrayAutoCloseDialogTurnOnClicked -> EventWrapper<NoExtraKeys>(
{ TabsTray.autoCloseTurnOnClicked.record(it) }
)
is Event.TabsTrayHasInactiveTabs -> EventWrapper(
{ TabsTray.hasInactiveTabs.record(it) },
{ TabsTray.hasInactiveTabsKeys.valueOf(it) }
)
is Event.TabsTrayCloseAllInactiveTabs -> EventWrapper<NoExtraKeys>(
{ TabsTray.closeAllInactiveTabs.record(it) }
)
is Event.TabsTrayCloseInactiveTab -> EventWrapper<NoExtraKeys>(
{ TabsTray.closeInactiveTab.add(amountClosed) }
)
is Event.TabsTrayOpenInactiveTab -> EventWrapper<NoExtraKeys>(
{ TabsTray.openInactiveTab.add() }
)
is Event.InactiveTabsSurveyOpened -> EventWrapper<NoExtraKeys>(
{ Preferences.inactiveTabsSurveyOpened.record(it) }
)
is Event.InactiveTabsOffSurvey -> EventWrapper(
{ Preferences.turnOffInactiveTabsSurvey.record(it) },
{ Preferences.turnOffInactiveTabsSurveyKeys.valueOf(it) }
)
is Event.AutoPlaySettingVisited -> EventWrapper<NoExtraKeys>(
{ Autoplay.visitedSetting.record(it) }
)
@ -704,6 +766,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.HomeScreenDisplayed -> EventWrapper<NoExtraKeys>(
{ HomeScreen.homeScreenDisplayed.record(it) }
)
is Event.HomeScreenCustomizedHomeClicked -> EventWrapper<NoExtraKeys>(
{ HomeScreen.customizeHomeClicked.record(it) }
)
is Event.TabViewSettingChanged -> EventWrapper(
{ Events.tabViewChanged.record(it) },
{ Events.tabViewChangedKeys.valueOf(it) }
@ -733,6 +798,34 @@ private val Event.wrapper: EventWrapper<*>?
{ RecentTabs.showAllClicked.record(it) }
)
is Event.RecentTabsSectionIsVisible -> EventWrapper<NoExtraKeys>(
{ RecentTabs.sectionVisible.set(true) }
)
is Event.RecentTabsSectionIsNotVisible -> EventWrapper<NoExtraKeys>(
{ RecentTabs.sectionVisible.set(false) }
)
is Event.BookmarkClicked -> EventWrapper<NoExtraKeys>(
{ RecentBookmarks.bookmarkClicked.add() }
)
is Event.ShowAllBookmarks -> EventWrapper<NoExtraKeys>(
{ RecentBookmarks.showAllBookmarks.add() }
)
is Event.RecentSearchesGroupDeleted -> EventWrapper<NoExtraKeys>(
{ RecentSearches.groupDeleted.record(it) }
)
is Event.RecentBookmarksShown -> EventWrapper<NoExtraKeys>(
{ RecentBookmarks.shown.record(it) }
)
is Event.RecentBookmarkCount -> EventWrapper<NoExtraKeys>(
{ RecentBookmarks.recentBookmarksCount.set(this.count.toLong()) },
)
is Event.AndroidAutofillRequestWithLogins -> EventWrapper<NoExtraKeys>(
{ AndroidAutofill.requestMatchingLogins.record(it) }
)
@ -757,6 +850,36 @@ private val Event.wrapper: EventWrapper<*>?
is Event.AndroidAutofillConfirmationSuccessful -> EventWrapper<NoExtraKeys>(
{ AndroidAutofill.confirmSuccessful.record(it) }
)
is Event.CreditCardSaved -> EventWrapper<NoExtraKeys>(
{ CreditCards.saved.add() }
)
is Event.CreditCardDeleted -> EventWrapper<NoExtraKeys>(
{ CreditCards.deleted.add() }
)
is Event.CreditCardModified -> EventWrapper<NoExtraKeys>(
{ CreditCards.modified.record(it) }
)
is Event.CreditCardFormDetected -> EventWrapper<NoExtraKeys>(
{ CreditCards.formDetected.record(it) }
)
is Event.CreditCardAutofillPromptShown -> EventWrapper<NoExtraKeys>(
{ CreditCards.autofillPromptShown.record(it) }
)
is Event.CreditCardAutofillPromptExpanded -> EventWrapper<NoExtraKeys>(
{ CreditCards.autofillPromptExpanded.record(it) }
)
is Event.CreditCardAutofillPromptDismissed -> EventWrapper<NoExtraKeys>(
{ CreditCards.autofillPromptDismissed.record(it) }
)
is Event.CreditCardAutofilled -> EventWrapper<NoExtraKeys>(
{ CreditCards.autofilled.record(it) }
)
is Event.CreditCardManagementAddTapped -> EventWrapper<NoExtraKeys>(
{ CreditCards.managementAddTapped.record(it) }
)
is Event.CreditCardManagementCardTapped -> EventWrapper<NoExtraKeys>(
{ CreditCards.managementCardTapped.record(it) }
)
// Don't record other events in Glean:
is Event.AddBookmark -> null

@ -5,9 +5,9 @@
package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts
import mozilla.components.compose.browser.awesomebar.AwesomeBarFacts as ComposeAwesomeBarFacts
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.feature.autofill.facts.AutofillFacts
import mozilla.components.feature.awesomebar.facts.AwesomeBarFacts
@ -19,7 +19,8 @@ import mozilla.components.feature.awesomebar.provider.SessionSuggestionProvider
import mozilla.components.feature.contextmenu.facts.ContextMenuFacts
import mozilla.components.feature.customtabs.CustomTabsFacts
import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.facts.LoginDialogFacts
import mozilla.components.feature.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
import mozilla.components.feature.search.telemetry.incontent.InContentTelemetry
@ -156,17 +157,27 @@ internal class ReleaseMetricController(
MetricServiceType.Marketing -> isMarketingDataTelemetryEnabled()
}
@Suppress("LongMethod")
private fun Fact.toEvent(): Event? = when (Pair(component, item)) {
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.DISPLAY -> Event.LoginDialogPromptDisplayed
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.CANCEL -> Event.LoginDialogPromptCancelled
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.NEVER_SAVE -> Event.LoginDialogPromptNeverSave
Component.FEATURE_PROMPTS to LoginDialogFacts.Items.SAVE -> Event.LoginDialogPromptSave
@Suppress("LongMethod", "MaxLineLength")
private fun Fact.toEvent(): Event? = when {
Component.FEATURE_PROMPTS == component && LoginDialogFacts.Items.DISPLAY == item -> Event.LoginDialogPromptDisplayed
Component.FEATURE_PROMPTS == component && LoginDialogFacts.Items.CANCEL == item -> Event.LoginDialogPromptCancelled
Component.FEATURE_PROMPTS == component && LoginDialogFacts.Items.NEVER_SAVE == item -> Event.LoginDialogPromptNeverSave
Component.FEATURE_PROMPTS == component && LoginDialogFacts.Items.SAVE == item -> Event.LoginDialogPromptSave
Component.FEATURE_PROMPTS == component && CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_FORM_DETECTED == item ->
Event.CreditCardFormDetected
Component.FEATURE_PROMPTS == component && CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SUCCESS == item ->
Event.CreditCardAutofilled
Component.FEATURE_PROMPTS == component && CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN == item ->
Event.CreditCardAutofillPromptShown
Component.FEATURE_PROMPTS == component && CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_EXPANDED == item ->
Event.CreditCardAutofillPromptExpanded
Component.FEATURE_PROMPTS == component && CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED == item ->
Event.CreditCardAutofillPromptDismissed
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
Component.FEATURE_CONTEXTMENU == component && ContextMenuFacts.Items.ITEM == item -> {
metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) }
}
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.TEXT_SELECTION_OPTION -> {
Component.FEATURE_CONTEXTMENU == component && ContextMenuFacts.Items.TEXT_SELECTION_OPTION == item -> {
when (metadata?.get("textSelectionOption")?.toString()) {
CONTEXT_MENU_COPY -> Event.ContextMenuCopyTapped
CONTEXT_MENU_SEARCH, CONTEXT_MENU_SEARCH_PRIVATELY -> Event.ContextMenuSearchTapped
@ -176,23 +187,23 @@ internal class ReleaseMetricController(
}
}
Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> {
Component.BROWSER_TOOLBAR == component && ToolbarFacts.Items.MENU == item -> {
metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } ?: Event.ToolbarMenuShown
}
Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> {
Component.BROWSER_MENU == component && BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM == item -> {
metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) }
}
Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.CLOSE -> Event.CustomTabsClosed
Component.FEATURE_CUSTOMTABS to CustomTabsFacts.Items.ACTION_BUTTON -> Event.CustomTabsActionTapped
Component.FEATURE_CUSTOMTABS == component && CustomTabsFacts.Items.CLOSE == item -> Event.CustomTabsClosed
Component.FEATURE_CUSTOMTABS == component && CustomTabsFacts.Items.ACTION_BUTTON == item -> Event.CustomTabsActionTapped
Component.FEATURE_MEDIA to MediaFacts.Items.NOTIFICATION -> {
Component.FEATURE_MEDIA == component && MediaFacts.Items.NOTIFICATION == item -> {
when (action) {
Action.PLAY -> Event.NotificationMediaPlay
Action.PAUSE -> Event.NotificationMediaPause
else -> null
}
}
Component.FEATURE_MEDIA to MediaFacts.Items.STATE -> {
Component.FEATURE_MEDIA == component && MediaFacts.Items.STATE == item -> {
when (action) {
Action.PLAY -> Event.MediaPlayState
Action.PAUSE -> Event.MediaPauseState
@ -200,7 +211,7 @@ internal class ReleaseMetricController(
else -> null
}
}
Component.SUPPORT_WEBEXTENSIONS to WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED -> {
Component.SUPPORT_WEBEXTENSIONS == component && WebExtensionFacts.Items.WEB_EXTENSIONS_INITIALIZED == item -> {
metadata?.get("installed")?.let { installedAddons ->
if (installedAddons is List<*>) {
settings.installedAddonsCount = installedAddons.size
@ -217,8 +228,8 @@ internal class ReleaseMetricController(
null
}
Component.BROWSER_AWESOMEBAR to BrowserAwesomeBarFacts.Items.PROVIDER_DURATION -> {
metadata?.get(BrowserAwesomeBarFacts.MetadataKeys.DURATION_PAIR)?.let { providerTiming ->
Component.BROWSER_AWESOMEBAR == component && ComposeAwesomeBarFacts.Items.PROVIDER_DURATION == item -> {
metadata?.get(ComposeAwesomeBarFacts.MetadataKeys.DURATION_PAIR)?.let { providerTiming ->
require(providerTiming is Pair<*, *>) { "Expected providerTiming to be a Pair" }
when (val provider = providerTiming.first as AwesomeBar.SuggestionProvider) {
is HistoryStorageSuggestionProvider -> PerfAwesomebar.historySuggestions
@ -236,13 +247,13 @@ internal class ReleaseMetricController(
}
null
}
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP -> {
Component.FEATURE_PWA == component && ProgressiveWebAppFacts.Items.HOMESCREEN_ICON_TAP == item -> {
Event.ProgressiveWebAppOpenFromHomescreenTap
}
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> {
Component.FEATURE_PWA == component && ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT == item -> {
Event.ProgressiveWebAppInstallAsShortcut
}
Component.FEATURE_TOP_SITES to TopSitesFacts.Items.COUNT -> {
Component.FEATURE_TOP_SITES == component && TopSitesFacts.Items.COUNT == item -> {
value?.let {
var count = 0
try {
@ -255,59 +266,59 @@ internal class ReleaseMetricController(
}
null
}
Component.FEATURE_SYNCEDTABS to SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED -> {
Component.FEATURE_SYNCEDTABS == component && SyncedTabsFacts.Items.SYNCED_TABS_SUGGESTION_CLICKED == item -> {
Event.SyncedTabSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.BOOKMARK_SUGGESTION_CLICKED == item -> {
Event.BookmarkSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.CLIPBOARD_SUGGESTION_CLICKED == item -> {
Event.ClipboardSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.HISTORY_SUGGESTION_CLICKED == item -> {
Event.HistorySuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.SEARCH_ACTION_CLICKED == item -> {
Event.SearchActionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.SEARCH_SUGGESTION_CLICKED == item -> {
Event.SearchSuggestionClicked
}
Component.FEATURE_AWESOMEBAR to AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED -> {
Component.FEATURE_AWESOMEBAR == component && AwesomeBarFacts.Items.OPENED_TAB_SUGGESTION_CLICKED == item -> {
Event.OpenedTabSuggestionClicked
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT -> {
Component.LIB_DATAPROTECT == component && SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT == item -> {
Event.SecurePrefsExperimentFailure(metadata?.get("javaClass") as String? ?: "null")
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.GET -> {
Component.LIB_DATAPROTECT == component && SecurePrefsReliabilityExperiment.Companion.Actions.GET == item -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsGetFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsGetSuccess(value ?: "")
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.WRITE -> {
Component.LIB_DATAPROTECT == component && SecurePrefsReliabilityExperiment.Companion.Actions.WRITE == item -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsWriteFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsWriteSuccess
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.RESET -> {
Component.LIB_DATAPROTECT == component && SecurePrefsReliabilityExperiment.Companion.Actions.RESET == item -> {
Event.SecurePrefsReset
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_ADD_CLICKED -> {
Component.FEATURE_SEARCH == component && AdsTelemetry.SERP_ADD_CLICKED == item -> {
Event.SearchAdClicked(value!!)
}
Component.FEATURE_SEARCH to AdsTelemetry.SERP_SHOWN_WITH_ADDS -> {
Component.FEATURE_SEARCH == component && AdsTelemetry.SERP_SHOWN_WITH_ADDS == item -> {
Event.SearchWithAds(value!!)
}
Component.FEATURE_SEARCH to InContentTelemetry.IN_CONTENT_SEARCH -> {
Component.FEATURE_SEARCH == component && InContentTelemetry.IN_CONTENT_SEARCH == item -> {
Event.SearchInContent(value!!)
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_REQUEST -> {
Component.FEATURE_AUTOFILL == component && AutofillFacts.Items.AUTOFILL_REQUEST == item -> {
val hasMatchingLogins = metadata?.get(AutofillFacts.Metadata.HAS_MATCHING_LOGINS) as Boolean?
if (hasMatchingLogins == true) {
Event.AndroidAutofillRequestWithLogins
@ -315,21 +326,21 @@ internal class ReleaseMetricController(
Event.AndroidAutofillRequestWithoutLogins
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_SEARCH -> {
Component.FEATURE_AUTOFILL == component && AutofillFacts.Items.AUTOFILL_SEARCH == item -> {
if (action == Action.SELECT) {
Event.AndroidAutofillSearchItemSelected
} else {
Event.AndroidAutofillSearchDisplayed
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_LOCK -> {
Component.FEATURE_AUTOFILL == component && AutofillFacts.Items.AUTOFILL_LOCK == item -> {
if (action == Action.CONFIRM) {
Event.AndroidAutofillUnlockSuccessful
} else {
Event.AndroidAutofillUnlockCanceled
}
}
Component.FEATURE_AUTOFILL to AutofillFacts.Items.AUTOFILL_CONFIRMATION -> {
Component.FEATURE_AUTOFILL == component && AutofillFacts.Items.AUTOFILL_CONFIRMATION == item -> {
if (action == Action.CONFIRM) {
Event.AndroidAutofillConfirmationSuccessful
} else {

@ -23,3 +23,26 @@ fun featureFlagPreference(key: String, default: Boolean, featureFlag: Boolean) =
} else {
DummyProperty()
}
private class LazyPreference(val key: String, val default: () -> Boolean) :
ReadWriteProperty<PreferencesHolder, Boolean> {
private val property: ReadWriteProperty<PreferencesHolder, Boolean> by lazy {
booleanPreference(key, default())
}
override fun getValue(thisRef: PreferencesHolder, property: KProperty<*>) =
this.property.getValue(thisRef, property)
override fun setValue(thisRef: PreferencesHolder, property: KProperty<*>, value: Boolean) =
this.property.setValue(thisRef, property, value)
}
/**
* Property delegate for getting and setting lazily a boolean shared preference gated by a feature flag.
*/
fun lazyFeatureFlagPreference(key: String, featureFlag: Boolean, default: () -> Boolean) =
if (featureFlag) {
LazyPreference(key, default)
} else {
DummyProperty()
}

@ -92,7 +92,11 @@ class DefaultBrowserToolbarController(
override fun handleToolbarClick() {
metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.BROWSER))
if (FeatureFlags.showHomeBehindSearch) {
// If we're displaying awesomebar search results, Home screen will not be visible (it's
// covered up with the search results). So, skip the navigation event in that case.
// If we don't, there's a visual flickr as we navigate to Home and then display search
// results on top it.
if (FeatureFlags.showHomeBehindSearch && currentSession?.content?.searchTerms.isNullOrBlank()) {
navController.navigate(
BrowserFragmentDirections.actionGlobalHome()
)

@ -4,6 +4,7 @@
package org.mozilla.fenix.components.toolbar
import android.graphics.Color
import android.view.HapticFeedbackConstants
import android.view.LayoutInflater
import android.view.View
@ -13,7 +14,6 @@ import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlinx.android.extensions.LayoutContainer
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.state.selector.selectedTab
@ -43,10 +43,7 @@ class BrowserToolbarView(
private val interactor: BrowserToolbarInteractor,
private val customTabSession: CustomTabSessionState?,
private val lifecycleOwner: LifecycleOwner
) : LayoutContainer {
override val containerView: View?
get() = container
) {
private val settings = container.context.settings()
@ -127,7 +124,7 @@ class BrowserToolbarView(
display.colors = display.colors.copy(
text = primaryTextColor,
securityIconSecure = primaryTextColor,
securityIconInsecure = primaryTextColor,
securityIconInsecure = Color.TRANSPARENT,
menu = primaryTextColor,
hint = secondaryTextColor,
separator = separatorColor,

@ -179,7 +179,7 @@ open class DefaultToolbarMenu(
val installToHomescreen = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_install_on_homescreen),
startImageResource = R.drawable.ic_add_to_homescreen,
startImageResource = R.drawable.mozac_ic_add_to_home_screen,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_install_on_homescreen),
@ -266,7 +266,7 @@ open class DefaultToolbarMenu(
val addToHomeScreenItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.ic_add_to_homescreen,
imageResource = R.drawable.mozac_ic_add_to_home_screen,
iconTintColorResource = primaryTextColor(),
isCollapsingMenuLimit = true
) {
@ -291,7 +291,7 @@ open class DefaultToolbarMenu(
val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
startImageResource = R.drawable.ic_settings,
startImageResource = R.drawable.mozac_ic_settings,
iconTintColorResource = if (hasAccountProblem)
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else
primaryTextColor(),

@ -5,8 +5,6 @@
package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.content.res.Configuration
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -104,49 +102,10 @@ class DefaultToolbarIntegration(
toolbar.display.menuBuilder = toolbarMenu.menuBuilder
toolbar.private = isPrivate
val drawable =
if (isPrivate) AppCompatResources.getDrawable(
context,
R.drawable.shield_dark
) else when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_UNDEFINED, // We assume light here per Android doc's recommendation
Configuration.UI_MODE_NIGHT_NO -> {
AppCompatResources.getDrawable(context, R.drawable.shield_light)
}
Configuration.UI_MODE_NIGHT_YES -> {
AppCompatResources.getDrawable(context, R.drawable.shield_dark)
}
else -> AppCompatResources.getDrawable(context, R.drawable.shield_light)
}
toolbar.display.indicators =
if (context.settings().shouldUseTrackingProtection) {
listOf(
DisplayToolbar.Indicators.TRACKING_PROTECTION,
DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY,
DisplayToolbar.Indicators.HIGHLIGHT
)
} else {
listOf(
DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY,
DisplayToolbar.Indicators.HIGHLIGHT
)
}
context.settings().shouldUseTrackingProtection
toolbar.display.icons = toolbar.display.icons.copy(
emptyIcon = null,
trackingProtectionTrackersBlocked = drawable!!,
trackingProtectionNothingBlocked = AppCompatResources.getDrawable(
context,
R.drawable.ic_tracking_protection_enabled
)!!,
trackingProtectionException = AppCompatResources.getDrawable(
context,
R.drawable.ic_tracking_protection_disabled
)!!
toolbar.display.indicators = listOf(
DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY,
DisplayToolbar.Indicators.HIGHLIGHT
)
val tabCounterMenu = FenixTabCounterMenu(
@ -154,8 +113,7 @@ class DefaultToolbarIntegration(
onItemTapped = {
interactor.onTabCounterMenuItemTapped(it)
},
iconColor =
if (isPrivate) {
iconColor = if (isPrivate) {
ContextCompat.getColor(context, R.color.primary_text_private_theme)
} else {
null

@ -0,0 +1,102 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import mozilla.components.ui.colors.PhotonColors
/**
* [Text] containing a substring styled as an URL informing when this is clicked.
*
* @param text Full text that will be displayed
* @param textColor [Color] of the normal text. The URL substring will have a default URL style applied.
* @param clickableStartIndex [text] index at which the URL substring starts.
* @param clickableEndIndex [text] index at which the URL substring ends.
* @param onClick Callback to be invoked only when the URL substring is clicked.
*/
@Composable
fun ClickableSubstringLink(
text: String,
textColor: Color,
clickableStartIndex: Int,
clickableEndIndex: Int,
onClick: () -> Unit
) {
val annotatedText = buildAnnotatedString {
append(text)
addStyle(
SpanStyle(textColor),
start = 0,
end = clickableStartIndex
)
addStyle(
SpanStyle(
color = when (isSystemInDarkTheme()) {
true -> PhotonColors.Violet40
false -> PhotonColors.Violet70
}
),
start = clickableStartIndex,
end = clickableEndIndex
)
addStyle(
SpanStyle(textColor),
start = clickableEndIndex,
end = text.length
)
addStyle(
SpanStyle(fontSize = 12.sp),
start = 0,
end = clickableEndIndex
)
addStringAnnotation(
tag = "link",
annotation = "",
start = clickableStartIndex,
end = clickableEndIndex
)
}
ClickableText(
text = annotatedText,
onClick = {
annotatedText
.getStringAnnotations("link", it, it)
.firstOrNull()?.let {
onClick()
}
}
)
}
@Composable
@Preview
private fun ClickableSubstringTextPreview() {
val text = "This text contains a link"
Box(modifier = Modifier.background(PhotonColors.White)) {
ClickableSubstringLink(
text,
PhotonColors.DarkGrey90,
text.indexOf("link"),
text.length
) { }
}
}

@ -0,0 +1,67 @@
/* 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.compose
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import mozilla.components.support.images.compose.loader.ImageLoader
import mozilla.components.support.images.compose.loader.WithImage
import org.mozilla.fenix.components.components
/**
* A composable that lays out and draws the image from a given URL while showing a default placeholder
* while that image is downloaded or a default fallback image when downloading failed.
*
* @param url URL from where the to download the image to be shown.
* @param modifier [Modifier] to be applied to the layout.
* @param private Whether or not this is a private request. Like in private browsing mode,
* private requests will not cache anything on disk and not send any cookies shared with the browser.
* @param targetSize Image size (width and height) the loaded image should be scaled to.
* @param contentDescription Localized text used by accessibility services to describe what this image represents.
* This should always be provided unless this image is used for decorative purposes, and does not represent
* a meaningful action that a user can take.
*/
@Composable
@Suppress("LongParameterList")
fun Image(
url: String,
modifier: Modifier = Modifier,
private: Boolean = false,
targetSize: Dp = 100.dp,
contentDescription: String? = null
) {
ImageLoader(
url = url,
client = components.core.client,
private = private,
targetSize = targetSize
) {
WithImage { painter ->
androidx.compose.foundation.Image(
painter = painter,
modifier = modifier,
contentDescription = contentDescription,
)
}
WithDefaultPlaceholder(modifier, contentDescription)
WithDefaultFallback(modifier, contentDescription)
}
}
@Composable
@Preview
private fun ImagePreview() {
Image(
"https://mozilla.com",
Modifier.height(100.dp).width(200.dp)
)
}

@ -0,0 +1,87 @@
/* 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.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import mozilla.components.support.images.compose.loader.Fallback
import mozilla.components.support.images.compose.loader.ImageLoaderScope
import mozilla.components.support.images.compose.loader.Placeholder
import mozilla.components.ui.colors.PhotonColors
/**
* Renders the app default image placeholder while the image is still getting loaded.
*
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
* @param contentDescription Text provided to accessibility services to describe what this image represents.
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
*/
@Composable
internal fun ImageLoaderScope.WithDefaultPlaceholder(
modifier: Modifier,
contentDescription: String? = null
) {
Placeholder {
DefaultImagePlaceholder(modifier, contentDescription)
}
}
/**
* Renders the app default image placeholder if loading the image failed.
*
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
* @param contentDescription Text provided to accessibility services to describe what this image represents.
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
*/
@Composable
internal fun ImageLoaderScope.WithDefaultFallback(
modifier: Modifier,
contentDescription: String? = null
) {
Fallback {
DefaultImagePlaceholder(modifier, contentDescription)
}
}
/**
* Application default image placeholder.
*
* @param modifier [Modifier] allowing to control among others the dimensions and shape of the image.
* @param contentDescription Text provided to accessibility services to describe what this image represents.
* Defaults to [null] suited for an image used only for decorative purposes and not to be read by
* accessibility services.
*/
@Composable
internal fun DefaultImagePlaceholder(
modifier: Modifier,
contentDescription: String? = null
) {
val color = when (isSystemInDarkTheme()) {
true -> PhotonColors.DarkGrey30
false -> PhotonColors.LightGrey30
}
Image(ColorPainter(color), contentDescription, modifier)
}
@Composable
@Preview
private fun DefaultImagePlaceholderPreview() {
DefaultImagePlaceholder(
Modifier
.size(200.dp, 100.dp)
.clip(RoundedCornerShape(8.dp))
)
}

@ -0,0 +1,47 @@
/* 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.compose
import androidx.compose.foundation.gestures.FlingBehavior
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
/**
* [FlingBehavior] for a [LazyRow] that will automatically scroll the list in the fling direction
* to fully show the next item.
*/
@Composable
fun EagerFlingBehavior(
lazyRowState: LazyListState
): FlingBehavior {
val scope = rememberCoroutineScope()
return LazyListEagerFlingBehavior(lazyRowState, scope)
}
private class LazyListEagerFlingBehavior(
private val lazyRowState: LazyListState,
private val scope: CoroutineScope
) : FlingBehavior {
override suspend fun ScrollScope.performFling(initialVelocity: Float): Float {
val firstItemIndex = lazyRowState.firstVisibleItemIndex
val itemIndexToScrollTo = when (initialVelocity <= 0) {
true -> firstItemIndex
false -> firstItemIndex + 1
}
scope.launch {
lazyRowState.animateScrollToItem(itemIndexToScrollTo)
}
return 0f // we've consumed the entire fling
}
}

@ -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.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Default layout of a large tab shown in a list taking String arguments for title and caption.
* Has the following structure:
* ```
* ---------------------------------------------
* | -------------- Title |
* | | Image | wrapped on |
* | | from | three rows if needed |
* | | imageUrl | |
* | -------------- Optional caption |
* ---------------------------------------------
* ```
*
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
* @param title Title off the tab this composable renders.
* @param caption Optional caption text.
* @param onClick Optional callback to be invoked when this composable is clicked.
*/
@Composable
fun ListItemTabLarge(
imageUrl: String,
title: String,
caption: String? = null,
onClick: (() -> Unit)? = null
) {
ListItemTabSurface(imageUrl, onClick) {
TabTitle(text = title, maxLines = 3)
if (caption != null) {
TabSubtitle(text = caption)
}
}
}
/**
* Default layout of a large tab shown in a list taking composable arguments for title and caption
* allowing as an exception to customize these elements.
* Has the following structure:
* ```
* ---------------------------------------------
* | -------------- -------------------------- |
* | | | | Title | |
* | | Image | | composable | |
* | | from | -------------------------- |
* | | imageUrl | -------------------------- |
* | | | | Optional composable | |
* | -------------- -------------------------- |
* ---------------------------------------------
* ```
*
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
* @param title Composable rendering the title of the tab this composable represents.
* @param subtitle Optional tab caption composable.
* @param onClick Optional callback to be invoked when this composable is clicked.
*/
@Composable
fun ListItemTabLarge(
imageUrl: String,
onClick: () -> Unit,
title: @Composable () -> Unit,
subtitle: @Composable (() -> Unit)? = null
) {
ListItemTabSurface(imageUrl, onClick) {
title()
subtitle?.invoke()
}
}
/**
* Shared default configuration of a ListItemTabLarge Composable.
*
* @param imageUrl URL from where the to download a header image of the tab this composable renders.
* @param onClick Optional callback to be invoked when this composable is clicked.
* @param tabDetails [Composable] Displayed to the the end of the image. Allows for variation in the item text style.
*/
@Composable
private fun ListItemTabSurface(
imageUrl: String,
onClick: (() -> Unit)? = null,
tabDetails: @Composable () -> Unit
) {
var modifier = Modifier.size(328.dp, 116.dp)
if (onClick != null) modifier = modifier.then(Modifier.clickable { onClick() })
Card(
modifier = modifier,
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.surface,
elevation = 6.dp
) {
Row(
modifier = Modifier.padding(16.dp)
) {
val (imageWidth, imageHeight) = 116.dp to 84.dp
val imageModifier = Modifier
.size(imageWidth, imageHeight)
.clip(RoundedCornerShape(8.dp))
Image(imageUrl, imageModifier, false, imageWidth)
Spacer(Modifier.width(16.dp))
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween
) {
tabDetails()
}
}
}
}
@Composable
@Preview
private fun ListItemTabLargePreview() {
FirefoxTheme {
ListItemTabLarge(
imageUrl = "",
title = "This is a very long title for a tab but needs to be so for this preview",
caption = "And this is a caption"
) { }
}
}

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
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
/**
* Placeholder of a [ListItemTabLarge] with the same dimensions but only a centered text.
* Has the following structure:
* ```
* ---------------------------------------------
* | |
* | |
* | Placeholder text |
* | |
* | |
* ---------------------------------------------
* ```
*
* @param text The only [String] that this will display.
* @param onClick Optional callback to be invoked when this composable is clicked.
*/
@Composable
fun ListItemTabLargePlaceholder(
text: String,
onClick: () -> Unit = { }
) {
Card(
modifier = Modifier
.size(328.dp, 116.dp)
.clickable { onClick() },
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.surface,
elevation = 6.dp,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = text,
color = FirefoxTheme.colors.textPrimary,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TextStyle(fontSize = 20.sp),
)
}
}
}
@Composable
@Preview
private fun ListItemTabLargePlaceholderPreview() {
FirefoxTheme {
ListItemTabLargePlaceholder(text = "Item placeholder")
}
}

@ -0,0 +1,79 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import org.mozilla.fenix.R
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Default layout for the header of a screen section.
*
* @param text [String] to be styled as header and displayed.
* @param modifier [Modifier] to be applied to the [Text].
*/
@Composable
fun SectionHeader(
text: String,
modifier: Modifier = Modifier
) {
Text(
modifier = modifier,
text = text,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
fontSize = 20.sp,
lineHeight = 20.sp
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = FirefoxTheme.colors.textPrimary
)
}
/**
* Default layout for the header of a screen section.
*
* @param text [String] to be styled as header and displayed.
* @param modifier [Modifier] to be applied to the [Text].
*/
@Composable
fun HomeSectionHeader(
text: String,
modifier: Modifier = Modifier
) {
Text(
modifier = modifier,
text = text,
style = TextStyle(
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
fontSize = 16.sp,
lineHeight = 20.sp
),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
color = FirefoxTheme.colors.textPrimary
)
}
@Composable
@Preview
private fun HeadingTextPreview() {
SectionHeader(text = "Section title")
}
@Composable
@Preview
private fun HomeHeadingTextPreview() {
HomeSectionHeader(text = "Home section title")
}

@ -0,0 +1,77 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.capitalize
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Default layout of a selectable chip.
*
* @param text [String] displayed in this chip. Ideally should only be one word.
* @param isSelected Whether this should be shown as selected.
* @param onClick Callback for when the user taps this.
*/
@Composable
fun SelectableChip(
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val contentColor = when (isSystemInDarkTheme()) {
true -> PhotonColors.LightGrey10
false -> if (isSelected) PhotonColors.LightGrey10 else PhotonColors.DarkGrey90
}
@Suppress("MagicNumber")
val backgroundColor = when (isSystemInDarkTheme()) {
true -> if (isSelected) PhotonColors.Violet50 else PhotonColors.DarkGrey50
// Custom color codes matching the Figma design.
false -> if (isSelected) { Color(0xFF312A65) } else { Color(0x1420123A) }
}
Box(
modifier = Modifier
.selectable(isSelected) { onClick() }
.clip(MaterialTheme.shapes.small)
.background(backgroundColor)
.padding(16.dp, 10.dp)
) {
Text(
text = text.capitalize(Locale.current),
style = TextStyle(fontSize = 14.sp),
color = contentColor
)
}
}
@Composable
@Preview
private fun SelectableChipPreview() {
FirefoxTheme {
Box(Modifier.fillMaxSize().background(FirefoxTheme.colors.surface)) {
SelectableChip("Chirp", false) { }
SelectableChip(text = "Chirp", isSelected = true) { }
}
}
}

@ -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/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Displays a list of items as a staggered horizontal grid placing them on ltr rows and continuing
* on as many below rows as needed to place all items.
*
* In an effort to best utilize the available row space this can mix the items such that narrower ones
* are placed on the same row as wider ones if the otherwise next item doesn't fit.
*
* @param modifier [Modifier] to be applied to the layout.
* @param horizontalItemsSpacing Minimum horizontal space between items. Does not add spacing to layout bounds.
* @param verticalItemsSpacing Vertical space between items
* @param arrangement How the items will be horizontally aligned and spaced.
* @param content The children composables to be laid out.
*/
@Composable
fun StaggeredHorizontalGrid(
modifier: Modifier = Modifier,
horizontalItemsSpacing: Dp = 0.dp,
verticalItemsSpacing: Dp = 8.dp,
arrangement: Arrangement.Horizontal = Arrangement.Start,
content: @Composable () -> Unit
) {
val currentLayoutDirection = LocalLayoutDirection.current
Layout(content, modifier) { items, constraints ->
val horizontalItemsSpacingPixels = horizontalItemsSpacing.roundToPx()
val verticalItemsSpacingPixels = verticalItemsSpacing.roundToPx()
var totalHeight = 0
val itemsRows = mutableListOf<List<Placeable>>()
val notYetPlacedItems = items.map {
it.measure(constraints)
}.toMutableList()
fun getIndexOfNextPlaceableThatFitsRow(available: List<Placeable>, currentWidth: Int): Int {
return available.indexOfFirst {
currentWidth + it.width <= constraints.maxWidth
}
}
// Populate each row with as many items as possible combining wider with narrower items.
// This will change the order of shown categories.
var (currentRow, currentWidth) = mutableListOf<Placeable>() to 0
while (notYetPlacedItems.isNotEmpty()) {
if (currentRow.isEmpty()) {
currentRow.add(
notYetPlacedItems[0].also {
currentWidth += it.width + horizontalItemsSpacingPixels
totalHeight += it.height + verticalItemsSpacingPixels
}
)
notYetPlacedItems.removeAt(0)
} else {
val nextPlaceableThatFitsIndex = getIndexOfNextPlaceableThatFitsRow(notYetPlacedItems, currentWidth)
if (nextPlaceableThatFitsIndex >= 0) {
currentRow.add(
notYetPlacedItems[nextPlaceableThatFitsIndex].also {
currentWidth += it.width + horizontalItemsSpacingPixels
}
)
notYetPlacedItems.removeAt(nextPlaceableThatFitsIndex)
} else {
itemsRows.add(currentRow)
currentRow = mutableListOf()
currentWidth = 0
}
}
}
if (currentRow.isNotEmpty()) {
itemsRows.add(currentRow)
}
totalHeight -= verticalItemsSpacingPixels
// Place each item from each row on screen.
layout(constraints.maxWidth, totalHeight) {
itemsRows.forEachIndexed { rowIndex, itemRow ->
val itemsSizes = IntArray(itemRow.size) {
itemRow[it].width + when (currentLayoutDirection == LayoutDirection.Ltr) {
true -> if (it < itemRow.lastIndex) horizontalItemsSpacingPixels else 0
false -> if (it > 0) horizontalItemsSpacingPixels else 0
}
}
val itemsPositions = IntArray(itemsSizes.size) { 0 }
with(arrangement) {
arrange(constraints.maxWidth, itemsSizes, currentLayoutDirection, itemsPositions)
}
itemRow.forEachIndexed { itemIndex, item ->
item.place(
x = itemsPositions[itemIndex],
y = (rowIndex * item.height) + (rowIndex * verticalItemsSpacingPixels)
)
}
}
}
}
}
@Composable
@Preview
private fun StaggeredHorizontalGridPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.surface)) {
StaggeredHorizontalGrid(
horizontalItemsSpacing = 8.dp,
arrangement = Arrangement.Center
) {
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor"
.split(" ")
.forEach {
Text(text = it, color = Color.Red, modifier = Modifier.border(3.dp, Color.Blue))
}
}
}
}
}

@ -0,0 +1,49 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
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
/**
* Default layout for a tab composable caption.
*
* @param text Tab caption.
* @param modifier Optional [Modifier] to be applied to the layout.
*/
@Composable
fun TabSubtitle(
text: String,
modifier: Modifier = Modifier
) {
Text(
modifier = modifier,
maxLines = 1,
text = text,
style = TextStyle(fontSize = 12.sp),
overflow = TextOverflow.Ellipsis,
color = FirefoxTheme.colors.textSecondary
)
}
@Composable
@Preview
private fun TabSubtitlePreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.surface)) {
TabSubtitle(
"Awesome tab subtitle",
)
}
}
}

@ -0,0 +1,95 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Special caption text for a tab layout shown on one line.
*
* This will combine [firstText] with a interdot and then [secondText] ensuring that the second text
* (which is assumed to be smaller) always fills as much space as needed with the [firstText] automatically
* being resized to be smaller with an added ellipsis characters if needed.
*
* Possible results:
* ```
* - when both texts would fit the screen
* ------------------------------------------
* |firstText · secondText |
* ------------------------------------------
*
* - when both text do not fit, second is shown in entirety, first is ellipsised.
* ------------------------------------------
* |longerFirstTextOrSmallSc... · secondText|
* ------------------------------------------
* ```
*
* @param firstText Text shown at the start of the row.
* @param secondText Text shown at the end of the row.
*/
@Composable
fun TabSubtitleWithInterdot(
firstText: String,
secondText: String,
) {
val currentLayoutDirection = LocalLayoutDirection.current
Layout(
content = {
TabSubtitle(text = firstText)
TabSubtitle(text = " \u00b7 ")
TabSubtitle(text = secondText)
}
) { items, constraints ->
// We need to measure from the end to start to ensure the secondItem will always be on screen
// and depending on secondItem's width and interdot's width the firstItem is automatically resized.
val secondItem = items[2].measure(constraints)
val interdot = items[1].measure(
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width)
)
val firstItem = items[0].measure(
constraints.copy(maxWidth = constraints.maxWidth - secondItem.width - interdot.width)
)
layout(constraints.maxWidth, constraints.maxHeight) {
val itemsPositions = IntArray(items.size)
with(Arrangement.Start) {
arrange(
constraints.maxWidth,
intArrayOf(firstItem.width, interdot.width, secondItem.width),
currentLayoutDirection,
itemsPositions
)
}
val placementHeight = constraints.maxHeight - firstItem.height
listOf(firstItem, interdot, secondItem).forEachIndexed { index, item ->
item.place(itemsPositions[index], placementHeight)
}
}
}
}
@Composable
@Preview
private fun TabSubtitleWithInterdotPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.surface)) {
TabSubtitleWithInterdot(
firstText = "firstText",
secondText = "secondText",
)
}
}
}

@ -0,0 +1,51 @@
/* 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.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Default layout for a tab composable title.
*
* @param text Tab title
* @param maxLines Maximum number of lines for [text] to span, wrapping if necessary.
* If the text exceeds the given number of lines it will be ellipsized.
* @param modifier Optional [Modifier] to be applied to the layout.
*/
@Composable
fun TabTitle(
text: String,
maxLines: Int,
modifier: Modifier = Modifier
) {
Text(
modifier = modifier,
maxLines = maxLines,
text = text,
style = TextStyle(fontSize = 14.sp),
overflow = TextOverflow.Ellipsis,
color = FirefoxTheme.colors.textPrimary
)
}
@Composable
private fun TabTitlePreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.surface)) {
TabTitle(
"Awesome tab title",
2
)
}
}
}

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

Loading…
Cancel
Save