Merge remote-tracking branch 'ssk97/main' into drag-tabs

drag-tabs
Adam Novak 3 years ago
commit ea68eab181

@ -2,7 +2,7 @@
name: "⌛ Performance issue"
about: Create a performance issue if the app is slow or it uses too much memory, disk space, battery, or network data
title: ""
labels: "eng:performance"
labels: "performance"
assignees: ''
---

@ -104,7 +104,6 @@ tasks:
tasks_for in ["action", "cron"]
|| (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"])
|| (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") && (head_branch[:8] != "mergify/")
|| (tasks_for == "github-release" && releaseAction == "published" && (ownerEmail != "mozilla-release-automation-bot@users.noreply.github.com") && (ownerEmail != "mozilla-release-automation-bot-staging@users.noreply.github.com"))
then:
$let:
level:

@ -53,14 +53,29 @@ ext.maybeConfigForJetpackBenchmark = { android ->
// WARNING: the benchmark framework warns you if you're running the test in a configuration
// that will compromise the accuracy of the results. Unfortunately, I couldn't get everything
// working so I had to suppress some things.
//
// - ACTIVITY-MISSING: we're supposed to use the test instrumentation runner,
// "androidx.benchmark.junit4.AndroidBenchmarkRunner". However, when I do so, I get an error
// that we're unable to launch the activity. My understanding is that this runner will use an
// "IsolationActivity" to reduce the impact of other work on the device from affecting the benchmark
// and to opt into a lower-max CPU frequency on unrooted devices that support it
// - UNLOCKED: ./gradlew lockClocks, which locks the CPU frequency, fails on my device. See
// https://issuetracker.google.com/issues/176836267 for potential workarounds.
testInstrumentationRunnerArgument 'androidx.benchmark.suppressErrors', 'ACTIVITY-MISSING,UNLOCKED'
testInstrumentationRunnerArguments = [
// - ACTIVITY-MISSING: we're supposed to use the test instrumentation runner,
// "androidx.benchmark.junit4.AndroidBenchmarkRunner". However, when I do so, I get an error
// that we're unable to launch the activity. My understanding is that this runner will use an
// "IsolationActivity" to reduce the impact of other work on the device from affecting the benchmark
// and to opt into a lower-max CPU frequency on unrooted devices that support it
// - UNLOCKED: ./gradlew lockClocks, which locks the CPU frequency, fails on my device. See
// https://issuetracker.google.com/issues/176836267 for potential workarounds.
'androidx.benchmark.suppressErrors' : 'ACTIVITY-MISSING,UNLOCKED',
// The tests don't always output a JSON file with the data. To make sure it does, we have to
// set androidx.benchmark.output.enable to true.
'androidx.benchmark.output.enable' : 'true',
// We set the the output directory simply for simplicity since the benchmark_runner.py script
// can't know the name of the phone in the /build/outputs/ directory. The system defaults to
// {phone_name} which can be troublesome finding in some case.
//
// NOTE: Jetpack Benchmark outputs to Logcat too. However, the output in the logcat is
// the min of the several repeats, for more statistics. Therefore, to get more stats,
// we refer to the JSON file.
additionalTestOutputDir : '/storage/emulated/0/benchmark'
]
}
}

@ -42,6 +42,7 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true'
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "GIT_HASH", "\"\"" // see override in release builds for why it's blank.
// This should be the "public" base URL of AMO.
buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\""
@ -83,6 +84,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
matchingFallbacks = ['release'] // Use on the "release" build type in dependencies (AARs)
// Changing the build config can cause files that depend on BuildConfig.java to recompile
// so we only set the git hash in release builds to avoid possible recompilation in debug builds.
buildConfigField "String", "GIT_HASH", "\"${Config.getGitHash()}\""
if (gradle.hasProperty("localProperties.autosignReleaseWithDebugKey")) {
signingConfig signingConfigs.debug
}
@ -271,6 +276,7 @@ android {
composeOptions {
kotlinCompilerExtensionVersion = Versions.androidx_compose
}
}
// -------------------------------------------------------------------------------------------------
@ -447,6 +453,12 @@ configurations {
jnaForTest
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
}
dependencies {
jnaForTest Deps.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)

File diff suppressed because it is too large Load Diff

@ -302,30 +302,6 @@ events:
- android-probes@mozilla.com
- erichards@mozilla.com
expires: never
tab_counter_menu_action:
type: event
description:
A tab counter menu item was tapped
extra_keys:
item:
description: |
A string containing the name of the item the user tapped. These items
are:
New tab, New private tab, Close tab
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11442
- https://github.com/mozilla-mobile/fenix/issues/19923
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11533
- https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877
- https://github.com/mozilla-mobile/fenix/pull/18143
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-12-01"
synced_tab_opened:
type: event
description: |
@ -623,47 +599,6 @@ toolbar_settings:
- android-probes@mozilla.com
expires: "2022-02-01"
crash_reporter:
opened:
type: event
description: |
The crash reporter was displayed
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1040
- https://github.com/mozilla-mobile/fenix/issues/19923
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708
- https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877
- https://github.com/mozilla-mobile/fenix/pull/18143
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-12-01"
closed:
type: event
description: |
The crash reporter was closed
extra_keys:
crash_submitted:
description: |
A boolean that tells us whether or not the user submitted a crash
report
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1040
- https://github.com/mozilla-mobile/fenix/issues/19923
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708
- https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877
- https://github.com/mozilla-mobile/fenix/pull/18143
- https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2021-12-01"
context_menu:
item_tapped:
type: event
@ -1323,6 +1258,7 @@ metrics:
lifetime: application
description: |
How many inactive tabs does the user have.
Value will be 0 if the feature is disabled.
send_in_pings:
- metrics
bugs:
@ -1815,6 +1751,19 @@ preferences:
notification_emails:
- android-probes@mozilla.com
expires: "2022-02-01"
search_term_groups_enabled:
type: boolean
description: |
Is search term group in tabs tray on?
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22057
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/22058
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"
search.default_engine:
code:
@ -2691,7 +2640,19 @@ history:
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"
search_term_group_tapped:
type: event
description: |
A user tapped on a search term group in history
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22299
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/22300
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"
reader_mode:
available:
@ -5919,7 +5880,7 @@ recent_searches:
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22175
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/TBD
- https://github.com/mozilla-mobile/fenix/pull/22176#issuecomment-956421788
data_sensitivity:
- interaction
notification_emails:
@ -6059,3 +6020,52 @@ credit_cards:
notification_emails:
- android-probes@mozilla.com
expires: "2022-09-01"
search_terms:
number_of_search_term_group:
type: event
description: |
Number of search term group when tabs tray is opened.
extra_keys:
count:
description: |
The number of tabs per search group
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22057
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/22058
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"
average_tabs_per_group:
type: event
description: |
Number of search term tabs per group when tabs tray is opened.
extra_keys:
count:
description: |
The average number of tabs per search group
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22057
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/22058
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"
jump_back_in_group_tapped:
type: event
description: |
User tapped on the jump back in search term group.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/22057
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/22058
data_sensitivity:
- interaction
notification_emails:
- android-probes@mozilla.com
expires: "2022-11-01"

@ -1,6 +1,6 @@
<html>
<body>
<a id="link" href="../resources/Globe.svg" download>Page content: Globe.svg</a>
<a id="link" href="../resources/tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFHPSD4wzCkRWiaOorNI574zLtv4Hjiz6O6T7onmUTGgUQ2YQoiQFyrCrPv8ZB9Kvmt.svg" download>Page content: tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFHPSD4wzCkRWiaOorNI574zLtv4Hjiz6O6T7onmUTGgUQ2YQoiQFyrCrPv8ZB9Kvmt.svg</a>
<script>
(function() {
document.getElementById("link").click()

@ -14,7 +14,7 @@ import mozilla.components.service.fxa.ServerConfig
object FxaServer {
private const val CLIENT_ID = "a2270f727f45f648"
const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
private const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
@Suppress("UNUSED_PARAMETER")
fun config(context: Context): ServerConfig {

@ -0,0 +1,36 @@
package org.mozilla.fenix.helpers
import android.content.Context
import androidx.test.platform.app.InstrumentationRegistry
import org.mozilla.fenix.ext.settings
class FeatureSettingsHelper {
private val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val settings = context.settings()
// saving default values of feature flags
private var isPocketEnabled: Boolean = settings.showPocketRecommendationsFeature
private var isJumpBackInCFREnabled: Boolean = settings.shouldShowJumpBackInCFR
private var isRecentTabsFeatureEnabled: Boolean = settings.showRecentTabsFeature
fun setPocketEnabled(enabled: Boolean) {
settings.showPocketRecommendationsFeature = enabled
}
fun setJumpBackCFREnabled(enabled: Boolean) {
settings.shouldShowJumpBackInCFR = enabled
}
fun setRecentTabsFeatureEnabled(enabled: Boolean) {
settings.showRecentTabsFeature = enabled
}
// Important:
// Use this after each test if you have modified these feature settings
// to make sure the app goes back to the default state
fun resetAllFeatureFlags() {
settings.showPocketRecommendationsFeature = isPocketEnabled
settings.shouldShowJumpBackInCFR = isJumpBackInCFREnabled
settings.showRecentTabsFeature = isRecentTabsFeatureEnabled
}
}

@ -17,6 +17,8 @@ object TestAssetHelper {
@Suppress("MagicNumber")
val waitingTime: Long = TimeUnit.SECONDS.toMillis(15)
val waitingTimeShort: Long = TimeUnit.SECONDS.toMillis(1)
// A long enough file name to not fit on a single line in the UI.
const val downloadFileName = "tAJwqaWjJsXS8AhzSninBMCfIZbHBGgcc001lx5DIdDwIcfEgQ6vE5Gb5VgAled17DFZ2A7ZDOHA0NpQPHXXFHPSD4wzCkRWiaOorNI574zLtv4Hjiz6O6T7onmUTGgUQ2YQoiQFyrCrPv8ZB9Kvmt.svg"
data class TestAsset(val url: Uri, val content: String, val title: String)
@ -70,7 +72,7 @@ object TestAssetHelper {
fun getDownloadAsset(server: MockWebServer): TestAsset {
val url = server.url("pages/download.html").toString().toUri()!!
val content = "Page content: Globe.svg"
val content = "Page content: $downloadFileName"
return TestAsset(url, content, "")
}

@ -16,7 +16,6 @@ import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.browser.customtabs.CustomTabsIntent
import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
@ -33,6 +32,7 @@ import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import java.io.File
import kotlinx.coroutines.runBlocking
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers
@ -43,7 +43,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
import org.mozilla.fenix.ui.robots.mDevice
import java.io.File
object TestHelper {
@ -71,13 +70,6 @@ object TestHelper {
).perform(longClick())
}
fun setPreference(context: Context, pref: String, value: Int) {
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
val editor = preferences.edit()
editor.putInt(pref, value)
editor.apply()
}
fun restartApp(activity: HomeActivityIntentTestRule) {
with(activity) {
finishActivity()

@ -6,7 +6,7 @@ import androidx.test.espresso.IdlingResource
import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment
class AddonsInstallingIdlingResource(
val fragmentManager: FragmentManager
private val fragmentManager: FragmentManager
) :
IdlingResource {
private var resourceCallback: IdlingResource.ResourceCallback? = null

@ -11,8 +11,6 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -25,11 +23,13 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.bookmarksMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.mDevice
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.swipeToBottom
import tools.fastlane.screengrab.Screengrab
import tools.fastlane.screengrab.locale.LocaleTestRule
class MenuScreenShotTest : ScreenshotTest() {
private lateinit var mockWebServer: MockWebServer
@ -194,8 +194,6 @@ fun editBookmarkFolder() = onView(withText(R.string.bookmark_menu_edit_button)).
fun deleteBookmarkFolder() = onView(withText(R.string.bookmark_menu_delete_button)).click()
fun saveToCollectionButton() = onView(withId(R.id.save_tab_group_button)).click()
fun tapOnTabCounter() = onView(withId(R.id.counter_text)).click()
fun settingsAccountPreferences() = onView(withText(R.string.preferences_sync)).click()

@ -166,6 +166,7 @@ class BookmarksTest {
addNewFolderName(bookmarksFolderName)
navigateUp()
verifyKeyboardHidden()
verifyBookmarkFolderIsNotCreated(bookmarksFolderName)
}
}
@ -608,9 +609,12 @@ class BookmarksTest {
}.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickEdit {
clickDeleteInEditModeButton()
cancelDeletion()
clickDeleteInEditModeButton()
confirmDeletion()
verifyDeleteSnackBarText()
verifyBookmarkIsDeleted("Test_Page_1")
}
}
}

@ -9,11 +9,10 @@ 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.FeatureSettingsHelper
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.ui.robots.browserScreen
@ -34,13 +33,17 @@ class CollectionTest {
private lateinit var mockWebServer: MockWebServer
private val firstCollectionName = "testcollection_1"
private val secondCollectionName = "testcollection_2"
private val featureSettingsHelper = FeatureSettingsHelper()
@get:Rule
val activityTestRule = HomeActivityTestRule()
@Before
fun setUp() {
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
// disabling these features to have better visibility of Collections
featureSettingsHelper.setRecentTabsFeatureEnabled(false)
featureSettingsHelper.setPocketEnabled(false)
featureSettingsHelper.setJumpBackCFREnabled(false)
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
@ -51,6 +54,9 @@ class CollectionTest {
@After
fun tearDown() {
mockWebServer.shutdown()
// resetting modified features enabled setting to default
featureSettingsHelper.resetAllFeatureFlags()
}
@Test
@ -81,7 +87,6 @@ class CollectionTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun verifyAddTabButtonOfCollectionMenu() {
val firstWebPage = getGenericAsset(mockWebServer, 1)
val secondWebPage = getGenericAsset(mockWebServer, 2)
@ -108,7 +113,6 @@ class CollectionTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun renameCollectionTest() {
val webPage = getGenericAsset(mockWebServer, 1)

@ -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
@ -32,7 +32,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*
*/
@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/18421")
class ContextMenusTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
@ -42,6 +41,7 @@ class ContextMenusTest {
@Before
fun setUp() {
activityIntentTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -67,7 +67,7 @@ class ContextMenusTest {
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick("Switch")
snackBarButtonClick()
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
@ -90,7 +90,7 @@ class ContextMenusTest {
verifyLinkContextMenuItems(genericURL.url)
clickContextOpenLinkInPrivateTab()
verifySnackBarText("New private tab opened")
snackBarButtonClick("Switch")
snackBarButtonClick()
verifyUrl(genericURL.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
@ -98,7 +98,6 @@ class ContextMenusTest {
}
}
@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/12473")
@Test
fun verifyContextCopyLink() {
val pageLinks =
@ -135,7 +134,6 @@ class ContextMenusTest {
}
}
@Ignore("Intermittent: https://github.com/mozilla-mobile/fenix/issues/12367")
@Test
fun verifyContextOpenImageNewTab() {
val pageLinks =
@ -150,13 +148,12 @@ class ContextMenusTest {
verifyLinkImageContextMenuItems(imageResource.url)
clickContextOpenImageNewTab()
verifySnackBarText("New tab opened")
snackBarButtonClick("Switch")
snackBarButtonClick()
verifyUrl(imageResource.url.toString())
}
}
@Test
@Ignore("Disabled Google Keyboard Clipboard overlay blocks the address bar: https://github.com/mozilla-mobile/fenix/issues/10586")
fun verifyContextCopyImageLocation() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
@ -177,7 +174,6 @@ class ContextMenusTest {
}
@Test
@Ignore("Intermittent: https://github.com/mozilla-mobile/fenix/issues/12309")
fun verifyContextSaveImage() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)
@ -202,7 +198,6 @@ class ContextMenusTest {
}
@Test
@Ignore("Intermittent: https://github.com/mozilla-mobile/fenix/issues/12309")
fun verifyContextMixedVariations() {
val pageLinks =
TestAssetHelper.getGenericAsset(mockWebServer, 4)

@ -99,7 +99,7 @@ class DeepLinkTest {
@Test
fun openSettings() {
robot.openSettings {
verifyBasicsHeading()
verifyGeneralHeading()
verifyAdvancedHeading()
}
}

@ -15,6 +15,7 @@ import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.downloadFileName
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -31,7 +32,6 @@ import org.mozilla.fenix.ui.robots.notificationShade
class DownloadTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@ -56,7 +56,7 @@ class DownloadTest {
fun tearDown() {
mockWebServer.shutdown()
TestHelper.deleteDownloadFromStorage("Globe.svg")
TestHelper.deleteDownloadFromStorage(downloadFileName)
}
@Test

@ -13,6 +13,7 @@ import mozilla.components.browser.storage.sync.PlacesHistoryStorage
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
@ -78,6 +79,7 @@ class HistoryTest {
}
}
@Ignore("Failing, see https://github.com/mozilla-mobile/fenix/issues/22304")
@Test
// Test running on beta/release builds in CI:
// caution when making changes to it, so they don't block the builds

@ -93,7 +93,7 @@ class HomeScreenTest {
verifyWelcomeHeader()
}.openThreeDotMenu {
}.openSettings {
verifyBasicsHeading()
verifyGeneralHeading()
}.goBack {
verifyExistingTopSitesList()
}

@ -1,22 +1,21 @@
package org.mozilla.fenix.ui
import android.view.View
import androidx.test.espresso.IdlingRegistry
import org.mozilla.fenix.helpers.TestAssetHelper
/* 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/. */
import android.view.View
import androidx.test.espresso.IdlingRegistry
import okhttp3.mockwebserver.MockWebServer
import org.junit.Rule
import org.junit.Before
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -80,7 +79,7 @@ class SettingsAddonsTest {
val addonName = "uBlock Origin"
navigationToolbar {}
.openNewTabAndEnterToBrowser(defaultWebPage.url) {}
.enterURLAndEnterToBrowser(defaultWebPage.url) {}
.openThreeDotMenu {}
.openAddonsManagerMenu {
addonsListIdlingResource =

@ -10,7 +10,6 @@ import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.FenixApplication
@ -64,33 +63,33 @@ class SettingsBasicsTest {
}
@Test
// Walks through settings menu and sub-menus to ensure all items are present
fun settingsMenuBasicsItemsTests() {
fun settingsGeneralItemsTests() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifySettingsToolbar()
verifyGeneralHeading()
verifySearchButton()
verifyTabsButton()
verifyHomepageButton()
verifyCustomizeButton()
verifyLoginsAndPasswordsButton()
verifyCreditCardsButton()
verifyAccessibilityButton()
verifyLanguageButton()
verifySetAsDefaultBrowserButton()
}
}
@Test
fun searchSettingsItemsTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyBasicsHeading()
verifySearchEngineButton()
verifyDefaultBrowserItem()
verifyTabsItem()
// drill down to submenu
}.openSearchSubMenu {
verifySearchToolbar()
verifyDefaultSearchEngineHeader()
verifySearchEngineList()
verifyShowSearchSuggestions()
verifyShowSearchShortcuts()
verifyShowClipboardSuggestions()
verifySearchBrowsingHistory()
verifySearchBookmarks()
}.goBack {
}.openCustomizeSubMenu {
verifyThemes()
}.goBack {
}.openAccessibilitySubMenu {
verifyAutomaticFontSizingMenuItems()
}.goBack {
// drill down to submenu
}
}
@ -137,7 +136,6 @@ class SettingsBasicsTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/19016")
@Test
fun changeThemeSetting() {
// Goes through the settings and changes the default search engine, then verifies it changes.

@ -449,7 +449,7 @@ class SettingsPrivacyTest {
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyBasicsHeading()
verifyGeneralHeading()
}
}
@ -472,7 +472,7 @@ class SettingsPrivacyTest {
confirmDeletionAndAssertSnackbar()
}
settingsScreen {
verifyBasicsHeading()
verifyGeneralHeading()
}.openSettingsSubMenuDeleteBrowsingData {
verifyOpenTabsDetails("0")
}.goBack {
@ -505,7 +505,7 @@ class SettingsPrivacyTest {
confirmDeletionAndAssertSnackbar()
verifyBrowsingHistoryDetails("0")
}.goBack {
verifyBasicsHeading()
verifyGeneralHeading()
}.goBack {
}
navigationToolbar {

@ -26,9 +26,11 @@ 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.FeatureSettingsHelper
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.downloadFileName
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
@ -68,7 +70,6 @@ class SmokeTest {
private var addonsListIdlingResource: RecyclerViewIdlingResource? = null
private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null
private var readerViewNotification: ViewVisibilityIdlingResource? = null
private val downloadFileName = "Globe.svg"
private val collectionName = "First Collection"
private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null
private var localeListIdlingResource: RecyclerViewIdlingResource? = null
@ -85,6 +86,8 @@ class SmokeTest {
private lateinit var browserStore: BrowserStore
private val featureSettingsHelper = FeatureSettingsHelper()
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityIntentTestRule(),
@ -108,7 +111,9 @@ 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
// disabling the new homepage pop-up that interferes with the tests.
featureSettingsHelper.setJumpBackCFREnabled(false)
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -144,6 +149,9 @@ class SmokeTest {
if (localeListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(localeListIdlingResource)
}
// resetting modified features enabled setting to default
featureSettingsHelper.resetAllFeatureFlags()
}
// Verifies the first run onboarding screen
@ -311,9 +319,6 @@ 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 {
@ -538,13 +543,12 @@ class SmokeTest {
}.openSearch {
verifyKeyboardVisibility()
clickSearchEngineShortcutButton()
mDevice.waitForIdle()
activityTestRule.waitForIdle()
verifyEnginesListShortcutContains(activityTestRule, "YouTube")
}
}
@Ignore("Started failing: https://github.com/mozilla-mobile/fenix/issues/21540")
@Ignore("Failing intermittently https://github.com/mozilla-mobile/fenix/issues/22256")
@Test
// Verifies setting as default a customized search engine name and URL
fun editCustomSearchEngineTest() {
@ -576,7 +580,6 @@ class SmokeTest {
}
}
@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
@ -821,10 +824,11 @@ class SmokeTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun createFirstCollectionTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
// disabling these features to have better visibility of Collections
featureSettingsHelper.setRecentTabsFeatureEnabled(false)
featureSettingsHelper.setPocketEnabled(false)
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -855,10 +859,11 @@ class SmokeTest {
}
@Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/21397")
fun verifyExpandedCollectionItemsTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
// disabling these features to have better visibility of Collections
featureSettingsHelper.setRecentTabsFeatureEnabled(false)
featureSettingsHelper.setPocketEnabled(false)
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -911,9 +916,6 @@ class SmokeTest {
@Test
fun shareCollectionTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -937,8 +939,6 @@ 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 {
@ -1100,7 +1100,6 @@ class SmokeTest {
}
@Test
@Ignore("To be re-enabled later. See https://github.com/mozilla-mobile/fenix/issues/20716")
fun mainMenuInstallPWATest() {
val pwaPage = "https://mozilla-mobile.github.io/testapp/"
@ -1275,7 +1274,7 @@ class SmokeTest {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PLAYING)
}.openTabDrawer {
verifyTabMediaControlButtonState("Pause")
clickTabMediaControlButton()
clickTabMediaControlButton("Pause")
verifyTabMediaControlButtonState("Play")
}.openTab(audioTestPage.title) {
assertPlaybackState(browserStore, MediaSession.PlaybackState.PAUSED)
@ -1357,8 +1356,6 @@ class SmokeTest {
@Test
fun goToHomeScreenBottomToolbarTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1371,9 +1368,6 @@ class SmokeTest {
@Test
fun goToHomeScreenTopToolbarTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
@ -1435,14 +1429,12 @@ class SmokeTest {
}.openTabsSubMenu {
verifyTabViewOptions()
verifyCloseTabsOptions()
verifyStartOnHomeOptions()
verifyMoveOldTabsToInactiveOptions()
}
}
@Test
fun alwaysStartOnHomeTest() {
val settings = activityTestRule.activity.applicationContext.settings()
settings.shouldShowJumpBackInCFR = false
fun startOnHomepageTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
@ -1450,8 +1442,8 @@ class SmokeTest {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openSettings {
}.openTabsSubMenu {
clickAlwaysStartOnHomeToggle()
}.openHomepageSubMenu {
clickStartOnHomepageButton()
}
restartApp(activityTestRule.activityRule)

@ -79,9 +79,12 @@ class StrictEnhancedTrackingProtectionTest {
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTabDrawer {
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
@ -97,9 +100,12 @@ class StrictEnhancedTrackingProtectionTest {
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTabDrawer {
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)
@ -113,7 +119,7 @@ class StrictEnhancedTrackingProtectionTest {
settingsSubMenuEnhancedTrackingProtection {
}.openExceptions {
verifyListedURL(trackingProtectionTest.url.toString())
verifyListedURL(trackingProtectionTest.url.host.toString())
}.disableExceptions {
verifyDefault()
}
@ -128,9 +134,12 @@ class StrictEnhancedTrackingProtectionTest {
// browsing a generic page to allow GV to load on a fresh run
navigationToolbar {
}.enterURLAndEnterToBrowser(genericPage.url) {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
}.openTabDrawer {
closeTab()
}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionTest.url) {}
enhancedTrackingProtection {
}.openEnhancedTrackingProtectionSheet {
verifyEnhancedTrackingProtectionSheetStatus("ON", true)

@ -12,8 +12,8 @@ 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.FeatureSettingsHelper
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.browserScreen
@ -41,6 +41,7 @@ import org.mozilla.fenix.ui.robots.notificationShade
class TabbedBrowsingTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer
private val featureSettingsHelper = FeatureSettingsHelper()
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@get:Rule
@ -48,7 +49,9 @@ class TabbedBrowsingTest {
@Before
fun setUp() {
activityTestRule.activity.applicationContext.settings().shouldShowJumpBackInCFR = false
// disabling the new homepage pop-up that interferes with the tests.
featureSettingsHelper.setJumpBackCFREnabled(false)
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -58,6 +61,7 @@ class TabbedBrowsingTest {
@After
fun tearDown() {
mockWebServer.shutdown()
featureSettingsHelper.resetAllFeatureFlags()
}
@Test
@ -65,7 +69,7 @@ class TabbedBrowsingTest {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.openNewTabAndEnterToBrowser(defaultWebPage.url) {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openTabDrawer {
@ -91,7 +95,7 @@ class TabbedBrowsingTest {
homeScreen {}.togglePrivateBrowsingMode()
navigationToolbar {
}.openNewTabAndEnterToBrowser(defaultWebPage.url) {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openTabDrawer {
@ -141,40 +145,54 @@ class TabbedBrowsingTest {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.openNewTabAndEnterToBrowser(genericURL.url) {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
closeTabViaXButton("Test_Page_1")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
closeTab()
}
mDevice.waitForIdle()
browserScreen {
homeScreen {
verifyNoTabsOpened()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
homeScreen {
verifyNoTabsOpened()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
}
homeScreen {
verifyNoTabsOpened()
}
}
@Test
fun verifyUndoSnackBarTest() {
// disabling these features because they interfere with the snackbar visibility
featureSettingsHelper.setPocketEnabled(false)
featureSettingsHelper.setRecentTabsFeatureEnabled(false)
mDevice.waitForIdle()
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
closeTab()
verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
verifyTabCounter("1")
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
}.closeTabDrawer { }
}
}
@Test
@ -183,40 +201,53 @@ class TabbedBrowsingTest {
homeScreen { }.togglePrivateBrowsingMode()
navigationToolbar {
}.openNewTabAndEnterToBrowser(genericURL.url) {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
closeTabViaXButton("Test_Page_1")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
closeTab()
}
mDevice.waitForIdle()
browserScreen {
homeScreen {
verifyNoTabsOpened()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
homeScreen {
verifyNoTabsOpened()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
}
homeScreen {
verifyNoTabsOpened()
}
}
mDevice.waitForIdle()
@Test
fun verifyPrivateTabUndoSnackBarTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
homeScreen { }.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
closeTab()
verifySnackBarText("Private tab closed")
snackBarButtonClick("UNDO")
}
mDevice.waitForIdle()
browserScreen {
verifyTabCounter("1")
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
verifyPrivateModeSelected()
}
}
@ -267,10 +298,16 @@ class TabbedBrowsingTest {
fun verifyEmptyTabTray() {
navigationToolbar {
}.openTabTray {
verifyNormalBrowsingButtonIsSelected(true)
verifyPrivateBrowsingButtonIsSelected(false)
verifySyncedTabsButtonIsSelected(false)
verifyNoTabsOpened()
verifyNewTabButton()
verifyTabTrayOverflowMenu(true)
}.toggleToPrivateTabs {
verifyNormalBrowsingButtonIsSelected(false)
verifyPrivateBrowsingButtonIsSelected(true)
verifySyncedTabsButtonIsSelected(false)
verifyNoTabsOpened()
verifyNewTabButton()
verifyTabTrayOverflowMenu(true)

@ -32,6 +32,7 @@ import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
@ -67,11 +68,15 @@ class BookmarksRobot {
assertFolderTitle(title)
}
fun verifyBookmarkFolderIsNotCreated(title: String) = assertBookmarkFolderIsNotCreated(title)
fun verifyBookmarkTitle(title: String) {
mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
assertBookmarkTitle(title)
}
fun verifyBookmarkIsDeleted(expectedTitle: String) = assertBookmarkIsDeleted(expectedTitle)
fun verifyDeleteSnackBarText() = assertSnackBarText("Deleted")
fun verifyUndoDeleteSnackBarButton() = assertUndoDeleteSnackBarButton()
@ -201,6 +206,12 @@ class BookmarksRobot {
fun longTapDesktopFolder(title: String) = onView(withText(title)).perform(longClick())
fun cancelDeletion() {
val cancelButton = mDevice.findObject(UiSelector().textContains("CANCEL"))
cancelButton.waitForExists(waitingTime)
cancelButton.click()
}
fun confirmDeletion() {
onView(withText(R.string.delete_browsing_data_prompt_allow))
.inRoot(RootMatchers.isDialog())
@ -318,6 +329,20 @@ private fun assertCloseButton() = closeButton().check(matches(withEffectiveVisib
private fun assertEmptyBookmarksList() =
onView(withId(R.id.bookmarks_empty_view)).check(matches(withText("No bookmarks here")))
private fun assertBookmarkFolderIsNotCreated(title: String) {
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/bookmarks_wrapper")
).waitForExists(waitingTime)
assertFalse(
mDevice.findObject(
UiSelector()
.textContains(title)
).waitForExists(waitingTime)
)
}
private fun assertBookmarkFavicon(forUrl: Uri) = bookmarkFavicon(forUrl.toString()).check(
matches(
withEffectiveVisibility(
@ -335,6 +360,19 @@ private fun assertFolderTitle(expectedTitle: String) =
private fun assertBookmarkTitle(expectedTitle: String) =
onView(withText(expectedTitle)).check(matches(isDisplayed()))
private fun assertBookmarkIsDeleted(expectedTitle: String) {
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/bookmarks_wrapper")
).waitForExists(waitingTime)
assertFalse(
mDevice.findObject(
UiSelector()
.textContains(expectedTitle)
).waitForExists(waitingTime)
)
}
private fun assertUndoDeleteSnackBarButton() =
snackBarUndoButton().check(matches(withText("UNDO")))

@ -12,7 +12,6 @@ import android.net.Uri
import android.os.SystemClock
import android.widget.EditText
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
@ -21,7 +20,6 @@ import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
@ -31,13 +29,13 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiObjectNotFoundException
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import mozilla.components.browser.state.selector.selectedTab
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.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.mozilla.fenix.R
@ -46,6 +44,7 @@ import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
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.TestHelper.waitForObjects
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull
@ -103,16 +102,23 @@ class BrowserRobot {
}
fun verifyTabCounter(expectedText: String) {
onView(withId(R.id.counter_text))
.check((matches(withText(containsString(expectedText)))))
val counter =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/counter_text")
.text(expectedText)
)
assertTrue(counter.waitForExists(waitingTime))
}
fun verifySnackBarText(expectedText: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(Until.findObject(text(expectedText)), waitingTime)
mDevice.waitForObjects(mDevice.findObject(UiSelector().textContains(expectedText)))
onView(withText(expectedText)).check(
matches(isCompletelyDisplayed())
assertTrue(
mDevice.findObject(
UiSelector()
.textContains(expectedText)
).waitForExists(waitingTime)
)
}
@ -170,13 +176,12 @@ class BrowserRobot {
fun verifyMenuButton() = assertMenuButton()
fun verifyNavURLBarItems() {
verifyEnhancedTrackingOptions()
pressBack()
waitingTime
verifySecureConnectionLockIcon()
verifyTabCounter("1")
verifyNavURLBar()
navURLBar().waitForExists(waitingTime)
verifyMenuButton()
verifyTabCounter("1")
verifySearchBar()
verifySecureConnectionLockIcon()
verifyHomeScreenButton()
}
fun verifyNoLinkImageContextMenuItems(containsURL: Uri) {
@ -199,6 +204,10 @@ class BrowserRobot {
)
}
fun verifyHomeScreenButton() = assertHomeScreenButton()
fun verifySearchBar() = assertSearchBar()
fun dismissContentContextMenu(containsURL: Uri) {
onView(withText(containsURL.toString()))
.inRoot(isDialog())
@ -314,11 +323,31 @@ class BrowserRobot {
}
fun longClickMatchingText(expectedText: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(Until.findObject(text(expectedText)), waitingTime)
try {
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime)
val link = mDevice.findObject(By.textContains(expectedText))
link.click(LONG_CLICK_DURATION)
} catch (e: NullPointerException) {
println(e)
val element = mDevice.findObject(text(expectedText))
element.click(LONG_CLICK_DURATION)
// Refresh the page in case the first long click didn't succeed
navigationToolbar {
}.openThreeDotMenu {
}.refreshPage {
mDevice.waitForIdle()
}
// Long click again the desired text
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/engineView"))
.waitForExists(waitingTime)
mDevice.findObject(UiSelector().textContains(expectedText)).waitForExists(waitingTime)
val link = mDevice.findObject(By.textContains(expectedText))
link.click(LONG_CLICK_DURATION)
}
}
fun longClickAndCopyText(expectedText: String, selectAll: Boolean = false) {
@ -374,10 +403,14 @@ class BrowserRobot {
}
}
fun snackBarButtonClick(expectedText: String) {
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
).perform(ViewActions.click())
fun snackBarButtonClick() {
val switchButton =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/snackbar_btn")
)
switchButton.waitForExists(waitingTime)
switchButton.clickAndWaitForNewWindow(waitingTime)
}
fun verifySaveLoginPromptIsShown() {
@ -405,12 +438,38 @@ class BrowserRobot {
.resourceId("password")
.className(EditText::class.java)
)
passwordField.waitForExists(waitingTime)
passwordField.click()
passwordField.clearTextField()
passwordField.text = password
// wait until the password is hidden
assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime))
try {
passwordField.waitForExists(waitingTime)
mDevice.findObject(
By
.res("password")
.clazz(EditText::class.java)
).click()
passwordField.clearTextField()
passwordField.text = password
// wait until the password is hidden
assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime))
} catch (e: UiObjectNotFoundException) {
println(e)
// Lets refresh the page and try again
browserScreen {
}.openThreeDotMenu {
}.refreshPage {
mDevice.waitForIdle()
}
} finally {
passwordField.waitForExists(waitingTime)
mDevice.findObject(
By
.res("password")
.clazz(EditText::class.java)
).click()
passwordField.clearTextField()
passwordField.text = password
// wait until the password is hidden
assertTrue(mDevice.findObject(UiSelector().text(password)).waitUntilGone(waitingTime))
}
}
fun clickMediaPlayerPlayButton() {
@ -497,7 +556,10 @@ class BrowserRobot {
}
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Tabs")))
mDevice.findObject(
UiSelector().descriptionContains("open tab. Tap to switch tabs.")
).waitForExists(waitingTime)
tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")))
@ -560,6 +622,15 @@ fun browserScreen(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
fun navURLBar() = mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
fun searchBar() = onView(withId(R.id.mozac_browser_toolbar_url_view))
fun homeScreenButton() = onView(withContentDescription(R.string.browser_toolbar_home))
private fun assertHomeScreenButton() =
homeScreenButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertSearchBar() = searchBar().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNavURLBar() = assertTrue(navURLBar().waitForExists(waitingTime))
private fun assertNavURLBarHidden() = assertTrue(navURLBar().waitUntilGone(waitingTime))
@ -586,7 +657,7 @@ private fun assertMenuButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun tabsCounter() = mDevice.findObject(By.desc("Tabs"))
private fun tabsCounter() = mDevice.findObject(By.res("$packageName:id/counter_root"))
private fun mediaPlayerPlayButton() =
mDevice.findObject(

@ -12,6 +12,7 @@ import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -132,6 +133,8 @@ private fun assertDownloadNotificationPopup() {
mDevice.waitNotNull(Until.findObjects(By.text("Open")), TestAssetHelper.waitingTime)
onView(withId(R.id.download_dialog_title))
.check(matches(withText(CoreMatchers.containsString("Download completed"))))
onView(withId(R.id.download_dialog_filename))
.check(matches(ViewMatchers.isCompletelyDisplayed()))
}
private fun closePromptButton() =

@ -9,7 +9,9 @@ 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
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry
@ -69,6 +71,7 @@ class EnhancedTrackingProtectionRobot {
fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionSheet().waitForExists(waitingTime)
openEnhancedTrackingProtectionSheet().click()
assertSecuritySheetIsCompletelyDisplayed()
EnhancedTrackingProtectionRobot().interact()
return Transition()
@ -169,3 +172,10 @@ private fun assertTrackingContentBlocked() {
}
private fun trackingContentBlockListButton() = onView(withId(R.id.tracking_content))
private fun assertSecuritySheetIsCompletelyDisplayed() {
mDevice.findObject(UiSelector().description("Quick settings sheet"))
.waitForExists(waitingTime)
onView(withContentDescription("Quick settings sheet"))
.check(matches(isCompletelyDisplayed()))
}

@ -187,8 +187,7 @@ class HomeScreenRobot {
}
fun openSearch(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
mDevice.findObject(UiSelector().resourceId("$packageName:id/toolbar"))
.waitForExists(waitingTime)
navigationToolbar().waitForExists(waitingTime)
navigationToolbar().click()
SearchRobot().interact()

@ -150,25 +150,6 @@ class NavigationToolbarRobot {
return TabDrawerRobot.Transition()
}
fun openNewTabAndEnterToBrowser(
url: Uri,
interact: BrowserRobot.() -> Unit
): BrowserRobot.Transition {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")), waitingTime)
urlBar().click()
awesomeBar().setText(url.toString())
mDevice.pressEnter()
runWithIdleRes(sessionLoadedIdlingResource) {
onView(ViewMatchers.withResourceName("browserLayout"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun visitLinkFromClipboard(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_toolbar_clear_view")),

@ -11,6 +11,7 @@ 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.junit4.android.ComposeNotIdleException
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onFirst
import androidx.compose.ui.test.onNodeWithTag
@ -56,7 +57,7 @@ class SearchRobot {
fun verifySearchView() = assertSearchView()
fun verifyBrowserToolbar() = assertBrowserToolbarEditView()
fun verifyScanButton() = assertScanButton()
fun verifySearchEngineButton() = assertSearchEngineButton()
fun verifySearchEngineButton() = assertSearchButton()
fun verifySearchWithText() = assertSearchWithText()
fun verifySearchEngineResults(rule: ComposeTestRule, searchEngineName: String, count: Int) =
assertSearchEngineResults(rule, searchEngineName, count)
@ -103,7 +104,12 @@ class SearchRobot {
}
fun typeSearch(searchTerm: String) {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view")
).waitForExists(waitingTime)
browserToolbarEditView().setText(searchTerm)
mDevice.waitForIdle()
}
@ -317,7 +323,7 @@ private fun assertScanButton() =
).waitForExists(waitingTime)
)
private fun assertSearchEngineButton() =
private fun assertSearchButton() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_engines_shortcut_button")
@ -346,16 +352,13 @@ fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
return SearchRobot.Transition()
}
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = {
mDevice.waitNotNull(
Until.findObject(
By.text("Search Engine")
),
waitingTime
)
private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean): () -> Unit = {
searchWrapper().waitForExists(waitingTime)
assertEquals(
"Keyboard not shown",
isExpectedToBeVisible,
UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true")
)
@ -387,21 +390,27 @@ private fun ComposeTestRule.assertSearchEngineList() {
@OptIn(ExperimentalTestApi::class)
private fun assertEngineListShortcutContains(rule: ComposeTestRule, searchEngineName: String) {
rule.waitForIdle()
mDevice.waitForObjects(
try {
rule.waitForIdle()
} catch (e: ComposeNotIdleException) {
mDevice.pressBack()
navigationToolbar {
}.clickUrlbar {
clickSearchEngineShortcutButton()
}
} finally {
mDevice.findObject(
UiSelector().textContains("Google")
)
)
).waitForExists(waitingTime)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
rule.onNodeWithTag("mozac.awesomebar.suggestions")
.performScrollToIndex(5)
rule.onNodeWithText(searchEngineName)
.assertExists()
.assertIsDisplayed()
.assertHasClickAction()
rule.onNodeWithText(searchEngineName)
.assertExists()
.assertIsDisplayed()
.assertHasClickAction()
}
}
private fun ComposeTestRule.selectDefaultSearchEngine(searchEngine: String) {
@ -434,5 +443,3 @@ private fun assertPastedToolbarText(expectedText: String) {
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun goBackButton() = onView(allOf(withContentDescription("Navigate up")))

@ -27,13 +27,18 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.textContains
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
import org.hamcrest.CoreMatchers
import org.junit.Assert.assertTrue
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.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsEnabled
import org.mozilla.fenix.helpers.click
@ -45,15 +50,17 @@ import org.mozilla.fenix.ui.robots.SettingsRobot.Companion.DEFAULT_APPS_SETTINGS
class SettingsRobot {
// BASICS SECTION
fun verifyBasicsHeading() = assertGeneralHeading()
fun verifyGeneralHeading() = assertGeneralHeading()
fun verifySearchEngineButton() = assertSearchEngineButton()
fun verifyThemeButton() = assertCustomizeButton()
fun verifySearchButton() = assertSearchButton()
fun verifyCustomizeButton() = assertCustomizeButton()
fun verifyThemeSelected() = assertThemeSelected()
fun verifyAccessibilityButton() = assertAccessibilityButton()
fun verifySetAsDefaultBrowserButton() = assertSetAsDefaultBrowserButton()
fun verifyDefaultBrowserItem() = assertDefaultBrowserItem()
fun verifyTabsItem() = assertTabsItem()
fun verifyTabsButton() = assertTabsButton()
fun verifyHomepageButton() = assertHomepageButton()
fun verifyCreditCardsButton() = assertCreditCardsButton()
fun verifyLanguageButton() = assertLanguageButton()
fun verifyDefaultBrowserIsDisaled() = assertDefaultBrowserIsDisabled()
fun clickDefaultBrowserSwitch() = toggleDefaultBrowserSwitch()
fun verifyAndroidDefaultAppsMenuAppears() = assertAndroidDefaultAppsMenuAppears()
@ -62,7 +69,7 @@ class SettingsRobot {
fun verifyPrivacyHeading() = assertPrivacyHeading()
fun verifyEnhancedTrackingProtectionButton() = assertEnhancedTrackingProtectionButton()
fun verifyLoginsButton() = assertLoginsButton()
fun verifyLoginsAndPasswordsButton() = assertLoginsAndPasswordsButton()
fun verifyEnhancedTrackingProtectionValue(state: String) =
assertEnhancedTrackingProtectionValue(state)
fun verifyPrivateBrowsingButton() = assertPrivateBrowsingButton()
@ -76,6 +83,7 @@ class SettingsRobot {
fun verifyOpenLinksInAppsButton() = assertOpenLinksInAppsButton()
fun verifyOpenLinksInAppsSwitchDefault() = assertOpenLinksInAppsValue()
fun verifySettingsView() = assertSettingsView()
fun verifySettingsToolbar() = assertSettingsToolbar()
// ADVANCED SECTION
fun verifyAdvancedHeading() = assertAdvancedHeading()
@ -88,8 +96,8 @@ class SettingsRobot {
// ABOUT SECTION
fun verifyAboutHeading() = assertAboutHeading()
fun verifyRateOnGooglePlay() = assertRateOnGooglePlay()
fun verifyAboutFirefoxPreview() = assertAboutFirefoxPreview()
fun verifyRateOnGooglePlay() = assertTrue(rateOnGooglePlayHeading().waitForExists(waitingTime))
fun verifyAboutFirefoxPreview() = assertTrue(aboutFirefoxHeading().waitForExists(waitingTime))
fun verifyGooglePlayRedirect() = assertGooglePlayRedirect()
class Transition {
@ -113,7 +121,7 @@ class SettingsRobot {
fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit):
SettingsSubMenuAboutRobot.Transition {
assertAboutFirefoxPreview().click()
aboutFirefoxHeading().click()
SettingsSubMenuAboutRobot().interact()
return SettingsSubMenuAboutRobot.Transition()
@ -147,6 +155,15 @@ class SettingsRobot {
return SettingsSubMenuTabsRobot.Transition()
}
fun openHomepageSubMenu(interact: SettingsSubMenuHomepageRobot.() -> Unit): SettingsSubMenuHomepageRobot.Transition {
mDevice.findObject(UiSelector().textContains("Homepage")).waitForExists(waitingTime)
onView(withText(R.string.preferences_home_2)).click()
SettingsSubMenuHomepageRobot().interact()
return SettingsSubMenuHomepageRobot.Transition()
}
fun openAccessibilitySubMenu(interact: SettingsSubMenuAccessibilityRobot.() -> Unit): SettingsSubMenuAccessibilityRobot.Transition {
scrollToElementByText("Accessibility")
@ -286,18 +303,37 @@ private fun assertSettingsView() {
}
// GENERAL SECTION
private fun assertSettingsToolbar() =
onView(
CoreMatchers.allOf(
withId(R.id.navigationToolbar),
hasDescendant(ViewMatchers.withContentDescription(R.string.action_bar_up_description)),
hasDescendant(withText(R.string.settings))
)
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGeneralHeading() {
scrollToElementByText("General")
onView(withText("General"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertSearchEngineButton() {
private fun assertSearchButton() {
mDevice.wait(Until.findObject(By.text("Search")), waitingTime)
onView(withText("Search"))
onView(withText(R.string.preferences_search))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertHomepageButton() =
onView(withText(R.string.preferences_home_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertCreditCardsButton() =
onView(withText(R.string.preferences_credit_cards)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertLanguageButton() =
onView(withText(R.string.preferences_language)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertCustomizeButton() = onView(withText("Customize"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -328,15 +364,9 @@ private fun assertAndroidDefaultAppsMenuAppears() {
intended(IntentMatchers.hasAction(DEFAULT_APPS_SETTINGS_ACTION))
}
private fun assertDefaultBrowserItem() {
mDevice.wait(Until.findObject(By.text("Set as default browser")), waitingTime)
onView(withText("Set as default browser"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTabsItem() {
private fun assertTabsButton() {
mDevice.wait(Until.findObject(By.text("Tabs")), waitingTime)
onView(withText("Tabs"))
onView(withText(R.string.preferences_tabs))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
@ -361,9 +391,9 @@ private fun assertEnhancedTrackingProtectionValue(state: String) {
onView(withText(state)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLoginsButton() {
private fun assertLoginsAndPasswordsButton() {
scrollToElementByText("Logins and passwords")
onView(withText("Logins and passwords"))
onView(withText(R.string.preferences_passwords_logins_and_passwords))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
@ -470,24 +500,32 @@ private fun assertAboutHeading(): ViewInteraction {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertRateOnGooglePlay(): ViewInteraction {
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("Rate on Google Play"))))
return onView(withText("Rate on Google Play"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun rateOnGooglePlayHeading(): UiObject {
val rateOnGooglePlay = mDevice.findObject(UiSelector().text("Rate on Google Play"))
scrollToElementByText("Rate on Google Play")
if (!rateOnGooglePlay.exists()) {
settingsList().swipeUp(2)
rateOnGooglePlay.waitForExists(waitingTime)
}
return rateOnGooglePlay
}
private fun assertAboutFirefoxPreview(): ViewInteraction {
onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About $appName"))))
return onView(withText("About $appName"))
.check(matches(isDisplayed()))
private fun aboutFirefoxHeading(): UiObject {
val aboutFirefoxHeading = mDevice.findObject(UiSelector().text("About $appName"))
scrollToElementByText("About $appName")
if (!aboutFirefoxHeading.exists()) {
settingsList().swipeUp(2)
aboutFirefoxHeading.waitForExists(waitingTime)
}
return aboutFirefoxHeading
}
fun swipeToBottom() = onView(withId(R.id.recycler_view)).perform(ViewActions.swipeUp())
fun clickRateButtonGooglePlay() {
assertRateOnGooglePlay().click()
rateOnGooglePlayHeading().click()
}
private fun assertGooglePlayRedirect() {
@ -502,3 +540,6 @@ private fun addonsManagerButton() = onView(withText(R.string.preferences_addons)
private fun goBackButton() =
onView(CoreMatchers.allOf(ViewMatchers.withContentDescription("Navigate up")))
private fun settingsList() =
UiScrollable(UiSelector().resourceId("$packageName:id/recycler_view"))

@ -67,12 +67,7 @@ private fun assertFirefoxPreviewPage() {
}
private fun navigateBackToAboutPage(itemToInteract: () -> Unit) {
browserScreen {
}.openTabDrawer {
closeTab()
}
homeScreen {
navigationToolbar {
}.openThreeDotMenu {
}.openSettings {
}.openAboutFirefoxPreview {
@ -161,8 +156,7 @@ private fun assertSupport() {
}
private fun assertCrashes() {
browserScreen {
navigationToolbar {
}.openThreeDotMenu {
}.openSettings {
}.openAboutFirefoxPreview {

@ -6,16 +6,19 @@ 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.withId
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 junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Enhanced Tracking Protection Exceptions sub menu.
@ -24,11 +27,11 @@ class SettingsSubMenuEnhancedTrackingProtectionExceptionsRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifyDefault() = assertExceptionDefault()!!
fun verifyDefault() = assertExceptionDefault()
fun verifyExceptionLearnMoreText() = assertExceptionLearnMoreText()!!
fun verifyExceptionLearnMoreText() = assertExceptionLearnMoreText()
fun verifyListedURL(url: String) = assertExceptionURL(url)!!
fun verifyListedURL(url: String) = assertExceptionURL(url)
fun verifyEnhancedTrackingProtectionProtectionExceptionsSubMenuItems() {
verifyDefault()
@ -63,13 +66,25 @@ private fun assertNavigationToolBarHeader() {
}
private fun assertExceptionDefault() =
onView(allOf(withText(R.string.exceptions_empty_message_description)))
assertTrue(
mDevice.findObject(
UiSelector().text("Exceptions let you disable tracking protection for selected sites.")
).waitForExists(waitingTime)
)
private fun assertExceptionLearnMoreText() =
onView(allOf(withText(R.string.exceptions_empty_message_learn_more_link)))
assertTrue(
mDevice.findObject(
UiSelector().text("Learn more")
).waitForExists(waitingTime)
)
private fun assertExceptionURL(url: String) =
onView(allOf(withText(url)))
assertTrue(
mDevice.findObject(
UiSelector().textContains(url.replace("http://", "https://"))
).waitForExists(waitingTime)
)
private fun disableExceptionsButton() =
onView(allOf(withId(R.id.removeAllExceptions)))
onView(withId(R.id.removeAllExceptions)).click()

@ -248,7 +248,7 @@ private fun assertCustomTrackingProtectionSettings() {
private fun cookiesCheckbox() = onView(withText("Cookies"))
private fun cookiesDropDownMenuDefault() = onView(withText("All cookies (will cause websites to break)"))
private fun cookiesDropDownMenuDefault() = onView(withText("Cross-site and social media trackers"))
private fun trackingContentCheckbox() = onView(withText("Tracking content"))

@ -0,0 +1,35 @@
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the settings Homepage sub menu.
*/
class SettingsSubMenuHomepageRobot {
fun clickStartOnHomepageButton() = homepageButton().click()
class Transition
}
private fun openingScreenHeading() = onView(withText(R.string.preferences_opening_screen))
private fun homepageButton() =
onView(
allOf(
withId(R.id.title),
withText(R.string.opening_screen_homepage),
hasSibling(withId(R.id.radio_button))
)
)
private fun lastTabButton() = onView(withText(R.string.opening_screen_last_tab))
private fun homepageAfterFourHoursButton() =
onView(withText(R.string.opening_screen_after_four_hours_of_inactivity))

@ -8,9 +8,7 @@ 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
@ -23,6 +21,7 @@ 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.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers
@ -30,12 +29,14 @@ 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
/**
* Implementation of Robot Pattern for the settings search sub menu.
*/
class SettingsSubMenuSearchRobot {
fun verifySearchToolbar() = assertSearchToolbar()
fun verifyDefaultSearchEngineHeader() = assertDefaultSearchEngineHeader()
fun verifySearchEngineList() = assertSearchEngineList()
fun verifyShowSearchSuggestions() = assertShowSearchSuggestions()
@ -73,12 +74,56 @@ class SettingsSubMenuSearchRobot {
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))
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
try {
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/edit_engine_name")
.text(engineName)
).waitForExists(waitingTime)
)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/edit_search_string")
.text(engineURL)
).waitForExists(waitingTime)
)
} catch (e: AssertionError) {
println("The name or the search string were not set properly")
// Lets again set both name and search string
goBackButton().click()
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).clear()
mDevice.findObject(By.res("$packageName:id/edit_engine_name")).setText(engineName)
mDevice.findObject(By.res("$packageName:id/edit_search_string")).clear()
mDevice.findObject(By.res("$packageName:id/edit_search_string")).setText(engineURL)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/edit_engine_name")
.text(engineName)
).waitForExists(waitingTime)
)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/edit_search_string")
.text(engineURL)
).waitForExists(waitingTime)
)
}
}
fun openEngineOverflowMenu(searchEngineName: String) {
@ -112,6 +157,15 @@ class SettingsSubMenuSearchRobot {
}
}
private fun assertSearchToolbar() =
onView(
allOf(
withId(R.id.navigationToolbar),
hasDescendant(withContentDescription(R.string.action_bar_up_description)),
hasDescendant(withText(R.string.preferences_search))
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertDefaultSearchEngineHeader() =
onView(withText("Default search engine"))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

@ -16,8 +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
import org.mozilla.fenix.R
/**
* Implementation of Robot Pattern for the settings Tabs sub menu.
@ -28,12 +27,7 @@ class SettingsSubMenuTabsRobot {
fun verifyCloseTabsOptions() = assertCloseTabsOptions()
fun verifyStartOnHomeOptions() = assertStartOnHomeOptions()
fun clickAlwaysStartOnHomeToggle() {
scrollToElementByText("Move old tabs to inactive")
alwaysStartOnHomeToggle().click()
}
fun verifyMoveOldTabsToInactiveOptions() = assertMoveOldTabsToInactiveOptions()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -57,11 +51,15 @@ private fun assertTabViewOptions() {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
searchTermTabGroupsToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
searchGroupsDescription()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertCloseTabsOptions() {
closeTabsHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
neverToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneDayToggle()
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterOneWeekToggle()
@ -70,14 +68,10 @@ private fun assertCloseTabsOptions() {
.check(ViewAssertions.matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertStartOnHomeOptions() {
// Scroll to ensure all the items are visible.
scrollToElementByText("Never")
startOnHomeHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
afterFourHoursToggle()
private fun assertMoveOldTabsToInactiveOptions() {
moveOldTabsToInactiveHeading()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
alwaysStartOnHomeToggle()
moveOldTabsToInactiveToggle()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
@ -89,23 +83,24 @@ private fun gridToggle() = onView(withText("Grid"))
private fun searchTermTabGroupsToggle() = onView(withText("Search groups"))
private fun searchGroupsDescription() = onView(withText("Group related sites together"))
private fun closeTabsHeading() = onView(withText("Close tabs"))
private fun manuallyToggle() = onView(withText("Manually"))
private fun neverToggle() = onView(withText("Never"))
private fun afterOneDayToggle() = onView(withText("After one day"))
private fun afterOneWeekToggle() = onView(withText("After one week"))
private fun afterOneMonthToggle() = onView(withText("After one month"))
private fun startOnHomeHeading() = onView(withText("Start on home"))
private fun afterFourHoursToggle() = onView(withText("After four hours"))
private fun alwaysStartOnHomeToggle() = onView(withText("Always"))
private fun moveOldTabsToInactiveHeading() = onView(withText("Move old tabs to inactive"))
private fun neverStartOnHomeToggle() = onView(withText("Never"))
private fun moveOldTabsToInactiveToggle() =
onView(withText(R.string.preferences_inactive_tabs_title))
private fun goBackButton() =
onView(allOf(ViewMatchers.withContentDescription("Navigate up")))

@ -8,11 +8,9 @@ package org.mozilla.fenix.ui.robots
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.GeneralLocation
@ -21,9 +19,7 @@ import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
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.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -36,19 +32,21 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior
import junit.framework.AssertionFailedError
import junit.framework.TestCase.assertTrue
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.mozilla.fenix.R
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.click
import org.mozilla.fenix.helpers.clickAtLocationInView
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
import org.mozilla.fenix.helpers.isSelected
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
@ -69,6 +67,12 @@ class TabDrawerRobot {
}
fun verifyNormalBrowsingButtonIsDisplayed() = assertNormalBrowsingButton()
fun verifyNormalBrowsingButtonIsSelected(isSelected: Boolean) =
assertNormalBrowsingButtonIsSelected(isSelected)
fun verifyPrivateBrowsingButtonIsSelected(isSelected: Boolean) =
assertPrivateBrowsingButtonIsSelected(isSelected)
fun verifySyncedTabsButtonIsSelected(isSelected: Boolean) =
assertSyncedTabsButtonIsSelected(isSelected)
fun verifyExistingOpenTabs(title: String) = assertExistingOpenTabs(title)
fun verifyCloseTabsButton(title: String) = assertCloseTabsButton(title)
@ -104,7 +108,12 @@ class TabDrawerRobot {
fun swipeTabRight(title: String) {
var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) {
while (!mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.text(title)
).waitUntilGone(waitingTimeShort) && retries < 2
) {
tab(title).perform(ViewActions.swipeRight())
retries++
}
@ -112,57 +121,105 @@ class TabDrawerRobot {
fun swipeTabLeft(title: String) {
var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) {
while (!mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.text(title)
).waitUntilGone(waitingTimeShort) && retries < 2
) {
tab(title).perform(ViewActions.swipeLeft())
retries++
}
}
fun closeTabViaXButton(title: String) {
mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
var retries = 0 // number of retries before failing, will stop at 2
do {
val closeButton = onView(
allOf(
withId(R.id.mozac_browser_tabstray_close),
withContentDescription("Close tab $title")
)
)
closeButton.perform(click())
retries++
} while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3)
}
fun verifySnackBarText(expectedText: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(findObject(By.text(expectedText)), TestAssetHelper.waitingTime)
assertTrue(
mDevice.findObject(
UiSelector().text(expectedText)
).waitForExists(waitingTime)
)
}
fun snackBarButtonClick(expectedText: String) {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/snackbar_btn")
).waitForExists(waitingTime)
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
).perform(click())
val snackBarButton =
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/snackbar_btn")
.text(expectedText)
)
snackBarButton.waitForExists(waitingTime)
snackBarButton.click()
}
fun verifyTabMediaControlButtonState(action: String) {
mDevice.waitForIdle()
try {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_empty_view")
).waitUntilGone(waitingTime)
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/play_pause_button")
).waitForExists(waitingTime)
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_grid_item")
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector().descriptionContains(action)
UiSelector()
.resourceId("$packageName:id/play_pause_button")
.descriptionContains(action)
).waitForExists(waitingTime)
)
assertTrue(
mDevice.findObject(UiSelector().descriptionContains(action)).waitForExists(waitingTime)
)
} catch (e: AssertionFailedError) {
// In some cases the tab media button isn't updated after performing an action on it
println("Failed to update the state of the tab media button")
// Let's dismiss the tabs tray and try again
mDevice.pressBack()
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/toolbar")
).waitForExists(waitingTime)
browserScreen {
}.openTabDrawer {
// Click again the tab media button
tabMediaControlButton().click()
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_empty_view")
).waitUntilGone(waitingTime)
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_tray_grid_item")
).waitForExists(waitingTime)
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/play_pause_button")
.descriptionContains(action)
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(UiSelector().descriptionContains(action)).waitForExists(waitingTime)
)
}
}
}
fun clickTabMediaControlButton() = tabMediaControlButton().click()
fun clickTabMediaControlButton(action: String) {
mDevice.waitNotNull(
Until.findObjects(
By
.res("$packageName:id/play_pause_button")
.descContains(action)
),
waitingTime
)
tabMediaControlButton().click()
}
fun clickSelectTabs() {
threeDotMenu().click()
@ -280,7 +337,11 @@ class TabDrawerRobot {
fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(findObject(text(title)))
tab(title).click()
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title)
).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -374,17 +435,19 @@ fun tabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
return TabDrawerRobot.Transition()
}
private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button))
private fun tabMediaControlButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/play_pause_button"))
private fun closeTabButton() = onView(withId(R.id.mozac_browser_tabstray_close))
private fun closeTabButton() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/mozac_browser_tabstray_close"))
private fun assertCloseTabsButton(title: String) =
onView(
allOf(
withId(R.id.mozac_browser_tabstray_close),
withContentDescription("Close tab $title")
)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_close")
.descriptionContains("Close tab $title")
).waitForExists(waitingTime)
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun normalBrowsingButton() = onView(
anyOf(
@ -394,6 +457,7 @@ private fun normalBrowsingButton() = onView(
)
private fun privateBrowsingButton() = onView(withContentDescription("Private tabs"))
private fun syncedTabsButton() = onView(withContentDescription("Synced tabs"))
private fun newTabButton() = mDevice.findObject(UiSelector().resourceId("$packageName:id/new_tab_button"))
private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
@ -406,22 +470,37 @@ private fun assertExistingOpenTabs(title: String) {
)
.waitForExists(waitingTime)
tab(title).check(matches(isDisplayed()))
} catch (e: NoMatchingViewException) {
onView(withId(R.id.tabsTray)).perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
allOf(
withId(R.id.mozac_browser_tabstray_title),
withText(title)
)
)
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title)
).waitForExists(waitingTime)
)
} catch (e: AssertionError) {
println("The tab wasn't found")
mDevice.findObject(UiSelector().resourceId("$packageName:id/tabsTray")).swipeUp(2)
assertTrue(
mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title)
).waitForExists(waitingTime)
)
}
}
private fun assertExistingTabList() =
onView(allOf(withId(R.id.tab_item)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun assertExistingTabList() {
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tabsTray")
).waitForExists(waitingTime)
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/tab_item")
).waitForExists(waitingTime)
)
}
private fun assertNoTabsOpenedText() =
onView(withId(R.id.tab_tray_empty_view))
@ -471,9 +550,24 @@ private fun assertNormalBrowsingButton() {
normalBrowsingButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertNormalBrowsingButtonIsSelected(isSelected: Boolean) {
normalBrowsingButton().check(matches(isSelected(isSelected)))
}
private fun assertPrivateBrowsingButtonIsSelected(isSelected: Boolean) {
privateBrowsingButton().check(matches(isSelected(isSelected)))
}
private fun assertSyncedTabsButtonIsSelected(isSelected: Boolean) {
syncedTabsButton().check(matches(isSelected(isSelected)))
}
private fun assertTabThumbnail() {
onView(withId(R.id.mozac_browser_tabstray_thumbnail))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_tabstray_thumbnail")
).waitForExists(waitingTime)
)
}
private fun tab(title: String) =

@ -80,8 +80,8 @@ 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.ext.actualInactiveTabs
import org.mozilla.fenix.perf.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.utils.Settings
/**
@ -198,7 +198,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
registerActivityLifecycleCallbacks(MarkersLifecycleCallbacks(components.core.engine))
registerActivityLifecycleCallbacks(MarkersActivityLifecycleCallbacks(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.
@ -428,22 +428,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
runOnlyInMainProcess {
components.core.icons.onTrimMemory(level)
// We want to be judicious in passing low mamory messages to
// android-components, because it is (at time of writing) hardcoded
// to drop tab states (and any user data in them) as soon as we
// reach "moderate" memory pressure on the system, even if the
// browser is in no danger of being killed. See
// https://github.com/mozilla-mobile/android-components/blob/38186676d46c555b5a24268e5fa361e45e57102c/components/browser/session/src/main/java/mozilla/components/browser/session/engine/middleware/TrimMemoryMiddleware.kt#L53-L64
// for the relvant android-components code and
// https://stuff.mit.edu/afs/sipb/project/android/docs/reference/android/content/ComponentCallbacks2.html
// for the list of memory pressure levels.
val settings = this.settings()
if (settings.shouldRelinquishMemoryUnderPressure) {
// We will give up our RAM when asked nicely
components.core.store.dispatch(SystemAction.LowMemoryAction(level))
}
// Otherwise we will die for our RAM, if pressed.
components.core.store.dispatch(SystemAction.LowMemoryAction(level))
}
}
@ -642,7 +627,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
tabViewSetting.set(settings.getTabViewPingString())
closeTabSetting.set(settings.getTabTimeoutPingString())
inactiveTabsCount.set(browserStore.state.inactiveTabs.size.toLong())
inactiveTabsCount.set(browserStore.state.actualInactiveTabs(settings).size.toLong())
val installSourcePackage = if (SDK_INT >= Build.VERSION_CODES.R) {
packageManager.getInstallSourceInfo(packageName).installingPackageName
@ -686,6 +672,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
voiceSearchEnabled.set(settings.shouldShowVoiceSearch)
openLinksInAppEnabled.set(settings.openLinksInExternalApp)
signedInSync.set(settings.signedInFxaAccount)
searchTermGroupsEnabled.set(settings.searchTermTabGroupsAreEnabled)
val syncedItems = SyncEnginesStorage(applicationContext).getStatus().entries.filter {
it.value

@ -35,7 +35,6 @@ import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
@ -98,7 +97,8 @@ 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.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
@ -187,6 +187,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
val startTimeProfiler = components.core.engine.profiler?.getProfilerTime()
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
MarkersFragmentLifecycleCallbacks.register(supportFragmentManager, components.core.engine)
// There is disk read violations on some devices such as samsung and pixel for android 9/10
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
// Theme setup should always be called before super.onCreate
@ -274,7 +276,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "HomeActivity.onCreate"
MarkersActivityLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "HomeActivity.onCreate"
)
StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE.
}
@ -339,7 +341,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
ProfilerMarkers.homeActivityOnStart(binding.rootContainer, components.core.engine.profiler)
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startProfilerTime, "HomeActivity.onStart"
MarkersActivityLifecycleCallbacks.MARKER_NAME, startProfilerTime, "HomeActivity.onStart"
) // DO NOT MOVE ANYTHING BELOW THIS addMarker CALL.
}
@ -913,7 +915,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) {
private fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) {
if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) {
window.addFlags(FLAG_SECURE)
} else {
@ -941,7 +943,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
isVisuallyComplete = true
}
private fun captureSnapshotTelemetryMetrics() = CoroutineScope(Dispatchers.IO).launch {
private fun captureSnapshotTelemetryMetrics() = CoroutineScope(IO).launch {
// PWA
val recentlyUsedPwaCount = components.core.webAppShortcutManager.recentlyUsedWebAppsCount(
activeThresholdMs = PWA_RECENTLY_USED_THRESHOLD

@ -20,7 +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.MarkersActivityLifecycleCallbacks
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.shortcut.NewTabShortcutIntentProcessor
@ -47,7 +47,7 @@ class IntentReceiverActivity : Activity() {
processIntent(intent)
components.core.engine.profiler?.addMarker(
MarkersLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "IntentReceiverActivity.onCreate"
MarkersActivityLifecycleCallbacks.MARKER_NAME, startTimeProfiler, "IntentReceiverActivity.onCreate"
)
StartupTimeline.onActivityCreateEndIntentReceiver() // DO NOT MOVE ANYTHING BELOW HERE.
}

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.action.WebExtensionAction
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
@ -53,7 +52,6 @@ class WebExtensionActionPopupFragment : AddonPopupBaseFragment(), EngineSession.
showToolbar(title)
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

@ -32,7 +32,6 @@ import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
@ -134,6 +133,7 @@ import org.mozilla.fenix.components.toolbar.interactor.BrowserToolbarInteractor
import org.mozilla.fenix.components.toolbar.interactor.DefaultBrowserToolbarInteractor
import org.mozilla.fenix.databinding.FragmentBrowserBinding
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.biometric.BiometricPromptFeature
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
@ -142,7 +142,6 @@ import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolb
* This class only contains shared code focused on the main browsing content.
* UI code specific to the app or to custom tabs can be found in the subclasses.
*/
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment :
Fragment(),
@ -212,6 +211,9 @@ abstract class BaseBrowserFragment :
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
customTabSessionId = requireArguments().getString(EXTRA_SESSION_ID)
// Diagnostic breadcrumb for "Display already aquired" crash:
@ -234,10 +236,17 @@ abstract class BaseBrowserFragment :
)
}
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "BaseBrowserFragment.onCreateView",
)
return binding.root
}
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
initializeUI(view)
if (customTabSessionId == null) {
@ -254,6 +263,11 @@ abstract class BaseBrowserFragment :
}
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "BaseBrowserFragment.onViewCreated",
)
}
private fun initializeUI(view: View) {
@ -1033,10 +1047,6 @@ abstract class BaseBrowserFragment :
components.useCases.sessionUseCases.reload()
}
hideToolbar()
components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)?.let {
updateThemeForSession(it)
}
}
@CallSuper

@ -13,7 +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.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
@ -44,7 +43,6 @@ import org.mozilla.fenix.theme.ThemeManager
/**
* Fragment used for browsing the web within the main app.
*/
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {

@ -10,7 +10,6 @@ import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
@ -30,7 +29,6 @@ import org.mozilla.fenix.utils.Settings
/**
* Displays an [InfoBanner] when a user visits a website that can be opened in an installed native app.
*/
@ExperimentalCoroutinesApi
@Suppress("LongParameterList")
class OpenInAppOnboardingObserver(
private val context: Context,

@ -12,14 +12,12 @@ import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
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
class CollectionCreationFragment : DialogFragment() {
private lateinit var collectionCreationView: CollectionCreationView
private lateinit var collectionCreationStore: CollectionCreationStore

@ -13,7 +13,6 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.Tab

@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.Store
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.components.appstate.AppStoreReducer
/**
* A [Store] that holds the [AppState] for the app and reduces [AppAction]s
* dispatched to the store.
*
* This store is not persisted to disk and is scoped to the life-cycle of the application.
*/
class AppStore(
initialState: AppState = AppState(),
middlewares: List<Middleware<AppState, AppAction>> = emptyList()
) : Store<AppState, AppAction>(initialState, AppStoreReducer::reduce, middlewares)

@ -106,13 +106,17 @@ class BackgroundServices(
SyncConfig(supportedEngines, PeriodicSyncConfig(periodMinutes = 240)) // four hours
private val creditCardKeyProvider by lazyMonitored { creditCardsStorage.value.crypto }
private val passwordKeyProvider by lazyMonitored { passwordsStorage.value.crypto }
init {
// Make the "history", "bookmark", "passwords", "tabs", "credit cards" stores
// accessible to workers spawned by the sync manager.
GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage)
GlobalSyncableStoreProvider.configureStore(
storePair = SyncEngine.Passwords to passwordsStorage,
keyProvider = lazy { passwordKeyProvider }
)
GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage)
GlobalSyncableStoreProvider.configureStore(
storePair = SyncEngine.CreditCards to creditCardsStorage,

@ -179,6 +179,7 @@ class Components(private val context: Context) {
val appStartReasonProvider by lazyMonitored { AppStartReasonProvider() }
val startupActivityLog by lazyMonitored { StartupActivityLog() }
val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) }
val appStore by lazyMonitored { AppStore() }
}
/**

@ -53,7 +53,6 @@ import mozilla.components.feature.webcompat.WebCompatFeature
import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.feature.webnotifications.WebNotificationFeature
import mozilla.components.lib.dataprotect.SecureAbove22Preferences
import mozilla.components.lib.dataprotect.generateEncryptionKey
import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.service.digitalassetlinks.local.StatementApi
import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker
@ -87,7 +86,6 @@ 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
/**
@ -294,7 +292,7 @@ class Core(
// We can fully initialize GeckoEngine without initialized our storage.
val lazyHistoryStorage = lazyMonitored { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazyMonitored { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, passwordsEncryptionKey) }
val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, lazySecurePrefs) }
val lazyAutofillStorage = lazyMonitored { AutofillCreditCardsAddressesStorage(context, lazySecurePrefs) }
/**
@ -418,23 +416,6 @@ class Core(
// Temporary. See https://github.com/mozilla-mobile/fenix/issues/19155
private val lazySecurePrefs = lazyMonitored { getSecureAbove22Preferences() }
private val passwordsEncryptionKey by lazyMonitored {
getSecureAbove22Preferences().getString(PASSWORDS_KEY)
?: generateEncryptionKey(KEY_STRENGTH).also {
if (context.settings().passwordsEncryptionKeyGenerated) {
// We already had previously generated an encryption key, but we have lost it
crashReporter.submitCaughtException(
IllegalStateException(
"Passwords encryption key for passwords storage was lost and we generated a new one"
)
)
}
context.settings().recordPasswordsEncryptionKeyGenerated()
getSecureAbove22Preferences().putString(PASSWORDS_KEY, it)
}
}
val trackingProtectionPolicyFactory = TrackingProtectionPolicyFactory(context.settings())
/**

@ -78,7 +78,7 @@ class FenixSnackbar private constructor(
companion object {
const val LENGTH_LONG = Snackbar.LENGTH_LONG
const val LENGTH_SHORT = Snackbar.LENGTH_SHORT
const val LENGTH_ACCESSIBLE = 15000 /* 15 seconds in ms */
private const val LENGTH_ACCESSIBLE = 15000 /* 15 seconds in ms */
const val LENGTH_INDEFINITE = Snackbar.LENGTH_INDEFINITE
private const val minTextSize = 12

@ -1,29 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components
import android.annotation.TargetApi
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.os.Build
import org.mozilla.gecko.search.SearchWidgetProvider
/**
* Handles the creation of the pinning search widget dialog.
*/
object SearchWidgetCreator {
/**
* Attempts to display a prompt requesting the user pin the search widget
* Returns true if the prompt is displayed successfully, and false otherwise.
*/
@TargetApi(Build.VERSION_CODES.O)
fun createSearchWidget(context: Context): Boolean {
val appWidgetManager: AppWidgetManager = context.getSystemService(AppWidgetManager::class.java)
val myProvider = ComponentName(context, SearchWidgetProvider::class.java)
return appWidgetManager.requestPinAppWidget(myProvider, null, null)
}
}

@ -63,7 +63,8 @@ class UseCases(
val searchUseCases by lazyMonitored {
SearchUseCases(
store,
tabsUseCases
tabsUseCases,
sessionUseCases
)
}

@ -0,0 +1,15 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate
import mozilla.components.lib.state.Action
import org.mozilla.fenix.components.AppStore
/**
* [Action] implementation related to [AppStore].
*/
sealed class AppAction : Action {
data class UpdateInactiveExpanded(val expanded: Boolean) : AppAction()
}

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate
import mozilla.components.lib.state.State
/**
* Value type that represents the state of the tabs tray.
*
* @property inactiveTabsExpanded A flag to know if the Inactive Tabs section of the Tabs Tray
* should be expanded when the tray is opened.
*/
data class AppState(
val inactiveTabsExpanded: Boolean = false
) : State

@ -0,0 +1,17 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.appstate
import org.mozilla.fenix.components.AppStore
/**
* Reducer for [AppStore].
*/
internal object AppStoreReducer {
fun reduce(state: AppState, action: AppAction): AppState = when (action) {
is AppAction.UpdateInactiveExpanded ->
state.copy(inactiveTabsExpanded = action.expanded)
}
}

@ -155,8 +155,10 @@ class DefaultPagedHistoryProvider(
// items.
val historyGroupsInOffset = if (history.isNotEmpty()) {
historyGroups?.filter {
history.last().visitedAt <= it.visitedAt - visitedAtBuffer &&
it.visitedAt - visitedAtBuffer <= (history.first().visitedAt + visitedAtBuffer)
it.items.any { item ->
(history.last().visitedAt - visitedAtBuffer) <= item.visitedAt &&
item.visitedAt <= (history.first().visitedAt + visitedAtBuffer)
}
} ?: emptyList()
} else {
emptyList()

@ -14,7 +14,6 @@ import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.Collections
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
@ -24,6 +23,7 @@ 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.SearchTerms
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites
@ -85,6 +85,7 @@ sealed class Event {
data class HistoryRecentSearchesTapped(val source: String) : Event() {
override val extras = mapOf(History.recentSearchesTappedKeys.pageNumber to source)
}
object HistorySearchTermGroupTapped : Event()
object ReaderModeAvailable : Event()
object ReaderModeOpened : Event()
object ReaderModeClosed : Event()
@ -618,14 +619,8 @@ sealed class Event {
}
}
object CrashReporterOpened : Event()
data class AddonInstalled(val addonId: String) : Event()
data class CrashReporterClosed(val crashSubmitted: Boolean) : Event() {
override val extras: Map<CrashReporter.closedKeys, String>?
get() = mapOf(CrashReporter.closedKeys.crashSubmitted to crashSubmitted.toString())
}
data class BrowserMenuItemTapped(val item: Item) : Event() {
enum class Item {
SETTINGS, HELP, DESKTOP_VIEW_ON, DESKTOP_VIEW_OFF, FIND_IN_PAGE, NEW_TAB,
@ -639,15 +634,6 @@ sealed class Event {
get() = mapOf(Events.browserMenuActionKeys.item to item.toString().lowercase(Locale.ROOT))
}
data class TabCounterMenuItemTapped(val item: Item) : Event() {
enum class Item {
NEW_TAB, NEW_PRIVATE_TAB, CLOSE_TAB
}
override val extras: Map<Events.tabCounterMenuActionKeys, String>?
get() = mapOf(Events.tabCounterMenuActionKeys.item to item.toString().lowercase(Locale.ROOT))
}
object AutoPlaySettingVisited : Event()
data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() {
@ -666,6 +652,18 @@ sealed class Event {
get() = mapOf(Events.tabViewChangedKeys.type to type.toString().lowercase(Locale.ROOT))
}
data class SearchTermGroupCount(val count: Int) : Event() {
override val extras: Map<SearchTerms.numberOfSearchTermGroupKeys, String>
get() = hashMapOf(SearchTerms.numberOfSearchTermGroupKeys.count to count.toString())
}
data class AverageTabsPerSearchTermGroup(val averageSize: Double) : Event() {
override val extras: Map<SearchTerms.averageTabsPerGroupKeys, String>
get() = hashMapOf(SearchTerms.averageTabsPerGroupKeys.count to averageSize.toString())
}
object JumpBackInGroupTapped : Event()
sealed class Search
internal open val extras: Map<*, String>?

@ -19,7 +19,6 @@ import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.ContextMenu
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
@ -44,6 +43,7 @@ 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.SearchTerms
import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SetDefaultNewtabExperiment
import org.mozilla.fenix.GleanMetrics.SetDefaultSettingExperiment
@ -157,13 +157,6 @@ private val Event.wrapper: EventWrapper<*>?
{ ContextMenu.itemTapped.record(it) },
{ ContextMenu.itemTappedKeys.valueOf(it) }
)
is Event.CrashReporterOpened -> EventWrapper<NoExtraKeys>(
{ CrashReporter.opened.record(it) }
)
is Event.CrashReporterClosed -> EventWrapper(
{ CrashReporter.closed.record(it) },
{ CrashReporter.closedKeys.valueOf(it) }
)
is Event.BrowserMenuItemTapped -> EventWrapper(
{ Events.browserMenuAction.record(it) },
{ Events.browserMenuActionKeys.valueOf(it) }
@ -319,6 +312,9 @@ private val Event.wrapper: EventWrapper<*>?
{ History.recentSearchesTapped.record(it) },
{ History.recentSearchesTappedKeys.valueOf(it) }
)
is Event.HistorySearchTermGroupTapped -> EventWrapper<NoExtraKeys>(
{ History.searchTermGroupTapped.record(it) }
)
is Event.CollectionRenamed -> EventWrapper<NoExtraKeys>(
{ Collections.renamed.record(it) }
)
@ -543,10 +539,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.VoiceSearchTapped -> EventWrapper<NoExtraKeys>(
{ VoiceSearch.tapped.record(it) }
)
is Event.TabCounterMenuItemTapped -> EventWrapper(
{ Events.tabCounterMenuAction.record(it) },
{ Events.tabCounterMenuActionKeys.valueOf(it) }
)
is Event.OnboardingPrivacyNotice -> EventWrapper<NoExtraKeys>(
{ Onboarding.privacyNotice.record(it) }
)
@ -880,6 +872,17 @@ private val Event.wrapper: EventWrapper<*>?
is Event.CreditCardManagementCardTapped -> EventWrapper<NoExtraKeys>(
{ CreditCards.managementCardTapped.record(it) }
)
is Event.SearchTermGroupCount -> EventWrapper(
{ SearchTerms.numberOfSearchTermGroup.record(it) },
{ SearchTerms.numberOfSearchTermGroupKeys.valueOf(it) }
)
is Event.AverageTabsPerSearchTermGroup -> EventWrapper(
{ SearchTerms.averageTabsPerGroup.record(it) },
{ SearchTerms.averageTabsPerGroupKeys.valueOf(it) }
)
is Event.JumpBackInGroupTapped -> EventWrapper<NoExtraKeys>(
{ SearchTerms.jumpBackInGroupTapped.record(it) }
)
// Don't record other events in Glean:
is Event.AddBookmark -> null
@ -942,8 +945,3 @@ class GleanMetricsService(
return event.wrapper != null
}
}
// Helper function for making our booleans fit into the string list formatting
fun Boolean.toStringList(): List<String> {
return listOf(this.toString())
}

@ -20,7 +20,7 @@ class SecurePrefsTelemetry(
private val appContext: Context,
private val experiments: NimbusApi
) {
suspend fun startTests() {
fun startTests() {
// The Android Keystore is used to secure the shared prefs only on API 23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// These tests should run only if the experiment is live

@ -19,11 +19,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.service.sync.logins.IdCollisionException
import mozilla.components.concept.storage.Login
import mozilla.components.service.sync.logins.InvalidRecordException
import mozilla.components.service.sync.logins.LoginsStorageException
import mozilla.components.service.sync.logins.ServerPassword
import mozilla.components.service.sync.logins.toLogin
import mozilla.components.support.migration.FennecLoginsMPImporter
import mozilla.components.support.migration.FennecProfile
import org.mozilla.fenix.R
@ -186,21 +184,16 @@ class MasterPasswordTipProvider(
}
}
private fun saveLogins(logins: List<ServerPassword>, dialog: AlertDialog) {
private fun saveLogins(logins: List<Login>, dialog: AlertDialog) {
CoroutineScope(IO).launch {
logins.map { it.toLogin() }.forEach {
try {
context.components.core.passwordsStorage.add(it)
} catch (e: InvalidRecordException) {
// This record was invalid and we couldn't save this login
context.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: IdCollisionException) {
// Nonempty ID was provided
context.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: LoginsStorageException) {
// Some other error occurred
context.components.analytics.crashReporter.submitCaughtException(e)
}
try {
context.components.core.passwordsStorage.importLoginsAsync(logins)
} catch (e: InvalidRecordException) {
// This record was invalid and we couldn't save this login
context.components.analytics.crashReporter.submitCaughtException(e)
} catch (e: LoginsStorageException) {
// Some other error occurred
context.components.analytics.crashReporter.submitCaughtException(e)
}
withContext(Dispatchers.Main) {
// Step 3: Dismiss this dialog and show the success dialog

@ -126,9 +126,6 @@ class DefaultBrowserToolbarController(
override fun handleTabCounterItemInteraction(item: TabCounterMenu.Item) {
when (item) {
is TabCounterMenu.Item.CloseTab -> {
metrics.track(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB)
)
store.state.selectedTab?.let {
// When closing the last tab we must show the undo snackbar in the home fragment
if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) {
@ -143,20 +140,12 @@ class DefaultBrowserToolbarController(
}
}
is TabCounterMenu.Item.NewTab -> {
metrics.track(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB)
)
activity.browsingModeManager.mode = BrowsingMode.Normal
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
is TabCounterMenu.Item.NewPrivateTab -> {
metrics.track(
Event.TabCounterMenuItemTapped(
Event.TabCounterMenuItemTapped.Item.NEW_PRIVATE_TAB
)
)
activity.browsingModeManager.mode = BrowsingMode.Private
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)

@ -14,7 +14,6 @@ import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.CustomTabSessionState
@ -35,7 +34,6 @@ import org.mozilla.fenix.utils.ToolbarPopupWindow
import java.lang.ref.WeakReference
import mozilla.components.browser.toolbar.behavior.ToolbarPosition as MozacToolbarPosition
@ExperimentalCoroutinesApi
@SuppressWarnings("LargeClass")
class BrowserToolbarView(
private val container: ViewGroup,

@ -11,7 +11,6 @@ import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.core.content.ContextCompat.getColor
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
@ -54,7 +53,6 @@ import org.mozilla.fenix.utils.BrowsersCache
* @param bookmarksStorage Used to check if a page is bookmarked.
*/
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ExperimentalCoroutinesApi
open class DefaultToolbarMenu(
private val context: Context,
private val store: BrowserStore,

@ -6,7 +6,6 @@ package org.mozilla.fenix.components.toolbar
import android.view.View
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
@ -16,7 +15,6 @@ import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
@ExperimentalCoroutinesApi
class MenuPresenter(
private val menuToolbar: BrowserToolbar,
private val store: BrowserStore,

@ -7,7 +7,6 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs
@ -28,7 +27,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager
@ExperimentalCoroutinesApi
abstract class ToolbarIntegration(
context: Context,
toolbar: BrowserToolbar,
@ -77,7 +75,6 @@ abstract class ToolbarIntegration(
}
}
@ExperimentalCoroutinesApi
class DefaultToolbarIntegration(
context: Context,
toolbar: BrowserToolbar,

@ -7,7 +7,10 @@ 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.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -27,6 +30,10 @@ import org.mozilla.fenix.components.components
* @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.
* @param alignment Optional alignment parameter used to place the [Painter] in the given
* bounds defined by the width and height.
* @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used
* if the bounds are a different size from the intrinsic size of the [Painter].
*/
@Composable
@Suppress("LongParameterList")
@ -35,7 +42,9 @@ fun Image(
modifier: Modifier = Modifier,
private: Boolean = false,
targetSize: Dp = 100.dp,
contentDescription: String? = null
contentDescription: String? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
) {
ImageLoader(
url = url,
@ -48,6 +57,8 @@ fun Image(
painter = painter,
modifier = modifier,
contentDescription = contentDescription,
alignment = alignment,
contentScale = contentScale
)
}

@ -7,7 +7,6 @@ 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
@ -28,41 +27,14 @@ fun SectionHeader(
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,
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
fontFamily = FontFamily(Font(R.font.metropolis_semibold)),
lineHeight = 20.sp,
overflow = TextOverflow.Ellipsis,
color = FirefoxTheme.colors.textPrimary
maxLines = 2
)
}
@ -71,9 +43,3 @@ fun HomeSectionHeader(
private fun HeadingTextPreview() {
SectionHeader(text = "Section title")
}
@Composable
@Preview
private fun HomeHeadingTextPreview() {
HomeSectionHeader(text = "Home section title")
}

@ -4,10 +4,14 @@
package org.mozilla.fenix.compose
import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.MaterialTheme
@ -15,7 +19,6 @@ 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
@ -38,16 +41,9 @@ fun SelectableChip(
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) }
false -> if (isSelected) PhotonColors.Ink20 else PhotonColors.LightGrey40
}
Box(
@ -57,6 +53,11 @@ fun SelectableChip(
.background(backgroundColor)
.padding(16.dp, 10.dp)
) {
val contentColor = when {
isSystemInDarkTheme() || isSelected -> PhotonColors.LightGrey10
else -> PhotonColors.DarkGrey90
}
Text(
text = text.capitalize(Locale.current),
style = TextStyle(fontSize = 14.sp),
@ -66,10 +67,29 @@ fun SelectableChip(
}
@Composable
@Preview
private fun SelectableChipPreview() {
@Preview(uiMode = UI_MODE_NIGHT_YES)
private fun SelectableChipDarkThemePreview() {
FirefoxTheme {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
SelectableChip("Chirp", false) { }
SelectableChip(text = "Chirp", isSelected = true) { }
}
}
}
@Composable
@Preview(uiMode = UI_MODE_NIGHT_NO)
private fun SelectableChipLightThemePreview() {
FirefoxTheme {
Box(Modifier.fillMaxSize().background(FirefoxTheme.colors.surface)) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(FirefoxTheme.colors.surface),
horizontalArrangement = Arrangement.SpaceEvenly
) {
SelectableChip("Chirp", false) { }
SelectableChip(text = "Chirp", isSelected = true) { }
}

@ -11,6 +11,7 @@ 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
@ -39,6 +40,7 @@ fun TabTitle(
}
@Composable
@Preview
private fun TabTitlePreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.surface)) {

@ -4,6 +4,7 @@
package org.mozilla.fenix.crashes
import android.util.Log
import androidx.navigation.NavController
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
@ -13,7 +14,6 @@ import kotlinx.coroutines.launch
import mozilla.components.lib.crash.Crash
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.Settings
@ -25,10 +25,6 @@ class CrashReporterController(
private val settings: Settings
) {
init {
components.analytics.metrics.track(Event.CrashReporterOpened)
}
/**
* Closes the crash reporter fragment and tries to recover the session.
*
@ -82,7 +78,7 @@ class CrashReporterController(
false
}
components.analytics.metrics.track(Event.CrashReporterClosed(didSubmitReport))
Log.i("Crash Reporter", "Report submitted: $didSubmitReport")
return job
}
}

@ -5,13 +5,11 @@
package org.mozilla.fenix.customtabs
import android.content.Context
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.feature.toolbar.ToolbarFeature
import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarMenu
@ExperimentalCoroutinesApi
class CustomTabToolbarIntegration(
context: Context,
toolbar: BrowserToolbar,

@ -11,7 +11,6 @@ import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.isVisible
import androidx.navigation.fragment.navArgs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.manifest.WebAppManifestParser
@ -41,7 +40,6 @@ import org.mozilla.fenix.ext.settings
/**
* Fragment used for browsing the web within external apps.
*/
@ExperimentalCoroutinesApi
class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val args by navArgs<ExternalAppBrowserFragmentArgs>()

@ -12,7 +12,6 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.plus
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.R
@ -69,7 +68,6 @@ class LoginExceptionsFragment : Fragment() {
}
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) {
exceptionsView.update(it.items)

@ -9,7 +9,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.lib.state.ext.consumeFrom
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
@ -61,7 +60,6 @@ class TrackingProtectionExceptionsFragment : Fragment() {
return binding.root
}
@ExperimentalCoroutinesApi
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
consumeFrom(exceptionsStore) {
exceptionsView.update(it.items)

@ -6,7 +6,6 @@ package org.mozilla.fenix.experiments
import android.content.Context
import org.mozilla.experiments.nimbus.mapKeysAsEnums
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getVariables
@ -51,10 +50,10 @@ class NimbusFeatures(private val context: Context) {
@Suppress("EnumNaming")
private enum class HomeScreenSection(val default: Boolean) {
topSites(true),
recentlySaved(false),
jumpBackIn(false),
pocket(false),
recentExplorations(false);
recentlySaved(true),
jumpBackIn(true),
pocket(true),
recentExplorations(true);
companion object {
/**
@ -62,12 +61,12 @@ class NimbusFeatures(private val context: Context) {
*/
fun toMap(context: Context): Map<HomeScreenSection, Boolean> {
return values().associate { section ->
val channelDefault = if (section == pocket && Config.channel.isNightlyOrDebug) {
val value = if (section == pocket) {
FeatureFlags.isPocketRecommendationsFeatureEnabled(context)
} else {
Config.channel.isNightlyOrDebug
section.default
}
section to (channelDefault || section.default)
section to value
}
}
}

@ -11,10 +11,22 @@ import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tabs.ext.hasMediaPlayed
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.tabstray.browser.TabGroup
import org.mozilla.fenix.tabstray.browser.maxActiveTime
import org.mozilla.fenix.tabstray.ext.isNormalTabActiveWithSearchTerm
import org.mozilla.fenix.tabstray.ext.isNormalTabInactive
import org.mozilla.fenix.utils.Settings
import java.util.concurrent.TimeUnit
import kotlin.math.max
/**
* The time until which a tab is considered in-active (in days).
*/
const val DEFAULT_ACTIVE_DAYS = 14L
/**
* The maximum time from when a tab was created or accessed until it is considered "inactive".
*/
val maxActiveTime = TimeUnit.DAYS.toMillis(DEFAULT_ACTIVE_DAYS)
/**
* Get the last opened normal tab, last tab with in progress media and last search term group, if available.
*
@ -60,7 +72,7 @@ val BrowserState.inProgressMediaTab: TabSessionState?
*/
val BrowserState.lastSearchGroup: RecentTab.SearchGroup?
get() {
val tabGroup = normalTabs.toSearchGroup().lastOrNull { it.tabs.count() > 1 } ?: return null
val tabGroup = normalTabs.toSearchGroup().first.lastOrNull() ?: return null
val firstTab = tabGroup.tabs.firstOrNull() ?: return null
return RecentTab.SearchGroup(
@ -73,9 +85,12 @@ val BrowserState.lastSearchGroup: RecentTab.SearchGroup?
}
/**
* Get search term groups sorted by last access time.
* Returns a pair containing a list of search term groups sorted by last access time, and "remainder" tabs that have
* search terms but should not be in groups (because the group is of size one).
*/
fun List<TabSessionState>.toSearchGroup(): List<TabGroup> {
fun List<TabSessionState>.toSearchGroup(
groupSet: Set<String> = emptySet()
): Pair<List<TabGroup>, List<TabSessionState>> {
val data = filter {
it.isNormalTabActiveWithSearchTerm(maxActiveTime)
}.groupBy {
@ -85,7 +100,7 @@ fun List<TabSessionState>.toSearchGroup(): List<TabGroup> {
}.lowercase()
}
return data.map { mapEntry ->
val groupings = data.map { mapEntry ->
val searchTerm = mapEntry.key.replaceFirstChar(Char::uppercase)
val groupTabs = mapEntry.value
val groupMax = groupTabs.fold(0L) { acc, tab ->
@ -97,5 +112,32 @@ fun List<TabSessionState>.toSearchGroup(): List<TabGroup> {
tabs = groupTabs,
lastAccess = groupMax
)
}.sortedBy { it.lastAccess }
}
val groups = groupings
.filter { it.tabs.size > 1 || groupSet.contains(it.searchTerm) }
.sortedBy { it.lastAccess }
val remainderTabs = (groupings - groups).flatMap { it.tabs }
return groups to remainderTabs
}
/**
* List of all inactive tabs based on [maxActiveTime].
* The user may have disabled the feature so for user interactions consider using the [actualInactiveTabs] method
* or an in place check of the feature status.
*/
val BrowserState.potentialInactiveTabs: List<TabSessionState>
get() = normalTabs.filter { it.isNormalTabInactive(maxActiveTime) }
/**
* List of all inactive tabs based on [maxActiveTime].
* The result will be always be empty if the user disabled the feature.
*/
fun BrowserState.actualInactiveTabs(settings: Settings): List<TabSessionState> {
return if (settings.inactiveTabsAreEnabled) {
potentialInactiveTabs
} else {
emptyList()
}
}

@ -72,9 +72,13 @@ fun Fragment.redirectToReAuth(
currentLocation: Int
) {
if (currentDestination !in destinations) {
// Workaround for memory leak caused by Android SDK bug
// https://issuetracker.google.com/issues/37125819
activity?.invalidateOptionsMenu()
when (currentLocation) {
R.id.loginDetailFragment,
R.id.editLoginFragment,
R.id.addLoginFragment,
R.id.savedLoginsFragment -> {
findNavController().popBackStack(R.id.savedLoginsAuthFragment, false)
}

@ -7,8 +7,8 @@ package org.mozilla.fenix.ext
import androidx.annotation.VisibleForTesting
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.home.HomeFragmentState
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.POCKET_STORIES_DEFAULT_CATEGORY_NAME
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
/**
* Get the list of stories to be displayed based on the user selected categories.
@ -86,7 +86,7 @@ internal fun getFilteredStoriesCount(
}
}
false -> {
return selectedCategories.map { it.name to it.stories.size }.toMap()
return selectedCategories.associate { it.name to it.stories.size }
}
}

@ -81,6 +81,7 @@ fun String.toShortUrl(publicSuffixList: PublicSuffixList): String {
}
// impl via FFTV https://searchfox.org/mozilla-mobile/source/firefox-echo-show/app/src/main/java/org/mozilla/focus/utils/FormattedDomain.java#129
@Suppress("DEPRECATION")
fun String.isIpv4(): Boolean = Patterns.IP_ADDRESS.matcher(this).matches()
// impl via FFiOS: https://github.com/mozilla-mobile/firefox-ios/blob/deb9736c905cdf06822ecc4a20152df7b342925d/Shared/Extensions/NSURLExtensions.swift#L292

@ -175,5 +175,5 @@ internal fun View.getKeyboardHeight(): Int {
* The assumed minimum height of the keyboard.
*/
@VisibleForTesting
@Dimension(unit = Dimension.DP)
@Dimension(unit = DP)
internal const val MINIMUM_KEYBOARD_HEIGHT = 100

@ -24,9 +24,9 @@ import org.mozilla.geckoview.GeckoRuntimeSettings
object GeckoProvider {
private var runtime: GeckoRuntime? = null
const val CN_UPDATE_URL =
private const val CN_UPDATE_URL =
"https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
const val CN_GET_HASH_URL =
private const val CN_GET_HASH_URL =
"https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
@Synchronized

@ -14,6 +14,7 @@ import mozilla.components.browser.state.selector.findNormalTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedNormalTab
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.SearchState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.search.ext.parseSearchTerms
import mozilla.components.lib.state.Middleware
@ -89,7 +90,8 @@ class HistoryMetadataMiddleware(
}
}
}
is EngineAction.LoadUrlAction -> {
is EngineAction.LoadUrlAction,
is EngineAction.OptimizedLoadUrlTriggeredAction -> {
// This isn't an ideal fix as we shouldn't have to hold any state in the middleware:
// https://github.com/mozilla-mobile/android-components/issues/11034
directLoadTriggered = true
@ -140,40 +142,48 @@ class HistoryMetadataMiddleware(
}
}
private fun createHistoryMetadata(context: MiddlewareContext<BrowserState, BrowserAction>, tab: TabSessionState) {
@Suppress("ComplexMethod")
private fun createHistoryMetadata(
context: MiddlewareContext<BrowserState, BrowserAction>,
tab: TabSessionState
) {
val tabParent = tab.getParent(context.store)
val previousUrlIndex = tab.content.history.currentIndex - 1
val tabMetadataHasSearchTerms = !tab.historyMetadata?.searchTerm.isNullOrBlank()
// Obtain search terms and referrer url either from tab parent, from the history stack, or
// from the tab itself.
// At a high level, there are two main cases here - 1) either the tab was opened as a 'new tab'
// via the search results page, or 2) a page was opened in the same tab as the search results page.
// Details about the New Tab case:
// - we obtain search terms via tab's parent (the search results page)
// - however, it's possible that parent changed (e.g. user navigated away from the search
// results page).
// - our approach below is to capture search terms from the parent within the tab.historyMetadata
// state on the first load of the tab, and then rely on this data for subsequent page loads on that tab.
// - this way, once a tab becomes part of the search group, it won't leave this search group
// unless a direct navigation event happens.
//
// At a high level, there are two main cases here:
// 1) The tab was opened as a 'new tab' via the search engine results page (SERP). In this
// case we obtain search terms via the tab's parent (the search results page). However, it's
// possible that the parent changed (e.g. user navigated away from the search results page).
// Our approach below is to capture search terms from the parent within the
// tab.historyMetadata state on the first load of the tab, and then rely on this data for
// subsequent page loads on that tab. This way, once a tab becomes part of the search group,
// it won't leave this group unless a direct navigation event happens.
//
// 2) A page was opened in the same tab as the search results page (navigated to via content).
val (searchTerm, referrerUrl) = when {
// Loading page opened in a New Tab for the first time.
// Page was opened in a new tab. Look for search terms in the parent tab.
tabParent != null && !tabMetadataHasSearchTerms -> {
val searchTerms = tabParent.content.searchTerms.takeUnless { it.isEmpty() }
?: context.state.search.parseSearchTerms(tabParent.content.url)
val searchTerms = findSearchTerms(tabParent, context.state.search)
searchTerms to tabParent.content.url
}
// We only want to inspect the previous url in history if the user navigated via
// web content i.e., they followed a link, not if the user navigated directly via
// toolbar.
// Page was navigated to via content i.e., the user followed a link. Look for search terms in tab history.
!directLoadTriggered && previousUrlIndex >= 0 -> {
// Once a tab is within the search group, only a direct load event (via the toolbar) can change that.
val previousUrl = tab.content.history.items[previousUrlIndex].uri
val (searchTerms, referrerUrl) = if (tabMetadataHasSearchTerms) {
tab.historyMetadata?.searchTerm to tab.historyMetadata?.referrerUrl
tab.historyMetadata?.searchTerm to previousUrl
} else {
val previousUrl = tab.content.history.items[previousUrlIndex].uri
context.state.search.parseSearchTerms(previousUrl) to previousUrl
// Find search terms by checking if page is a SERP or a result opened from a SERP
val searchTerms = findSearchTerms(tab, context.state.search)
if (searchTerms != null) {
searchTerms to null
} else {
context.state.search.parseSearchTerms(previousUrl) to previousUrl
}
}
if (searchTerms != null) {
@ -189,11 +199,8 @@ class HistoryMetadataMiddleware(
tabMetadataHasSearchTerms && !(directLoadTriggered && previousUrlIndex >= 0) -> {
tab.historyMetadata?.searchTerm to tab.historyMetadata?.referrerUrl
}
// We had no search terms, no history stack, and no parent.
// This would be the case for any page loaded directly via the toolbar including
// a search results page itself. For now, the original search results page is not
// part of the search group: https://github.com/mozilla-mobile/fenix/issues/21659.
else -> null to null
// In all other cases (e.g. direct load) find search terms by checking if page is a SERP
else -> findSearchTerms(tab, context.state.search) to null
}
// Sanity check to make sure we don't record a metadata record referring to itself.
@ -217,4 +224,8 @@ class HistoryMetadataMiddleware(
store.state.findTab(it)
}
}
private fun findSearchTerms(tab: TabSessionState, searchState: SearchState): String? {
return tab.content.searchTerms.takeUnless { it.isEmpty() } ?: searchState.parseSearchTerms(tab.content.url)
}
}

@ -46,7 +46,6 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
@ -94,8 +93,8 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.databinding.FragmentHomeBinding
import org.mozilla.fenix.datastore.pocketStoriesSelectedCategoriesDataStore
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.experiments.FeatureId
import org.mozilla.fenix.ext.asRecentTabs
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics
@ -107,6 +106,8 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.historymetadata.HistoryMetadataFeature
import org.mozilla.fenix.historymetadata.controller.DefaultHistoryMetadataController
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.recentbookmarks.RecentBookmarksFeature
import org.mozilla.fenix.home.recentbookmarks.controller.DefaultRecentBookmarksController
import org.mozilla.fenix.home.recenttabs.RecentTab
@ -116,10 +117,9 @@ import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.DefaultPocketStoriesController
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView
import org.mozilla.fenix.home.topsites.DefaultTopSitesView
import org.mozilla.fenix.onboarding.FenixOnboarding
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
@ -130,7 +130,6 @@ import org.mozilla.fenix.whatsnew.WhatsNew
import java.lang.ref.WeakReference
import kotlin.math.min
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
class HomeFragment : Fragment() {
private val args by navArgs<HomeFragmentArgs>()
@ -170,7 +169,7 @@ class HomeFragment : Fragment() {
private lateinit var homeFragmentStore: HomeFragmentStore
private var _sessionControlInteractor: SessionControlInteractor? = null
protected val sessionControlInteractor: SessionControlInteractor
private val sessionControlInteractor: SessionControlInteractor
get() = _sessionControlInteractor!!
private var sessionControlView: SessionControlView? = null
@ -186,6 +185,9 @@ class HomeFragment : Fragment() {
internal var getMenuButton: () -> MenuButton? = { binding.menuButton }
override fun onCreate(savedInstanceState: Bundle?) {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
super.onCreate(savedInstanceState)
bundleArgs = args.toBundle()
@ -201,6 +203,11 @@ class HomeFragment : Fragment() {
) {
showPrivacyPopWindow(requireContext(), requireActivity())
}
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreate",
)
}
@Suppress("LongMethod")
@ -209,6 +216,9 @@ class HomeFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
_binding = FragmentHomeBinding.inflate(inflater, container, false)
val activity = activity as HomeActivity
val components = requireComponents
@ -334,8 +344,7 @@ class HomeFragment : Fragment() {
hideOnboarding = ::hideOnboardingAndOpenSearch,
registerCollectionStorageObserver = ::registerCollectionStorageObserver,
removeCollectionWithUndo = ::removeCollectionWithUndo,
showTabTray = ::openTabsTray,
handleSwipedItemDeletionCancel = ::handleSwipedItemDeletionCancel
showTabTray = ::openTabsTray
),
recentTabController = DefaultRecentTabsController(
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
@ -379,6 +388,11 @@ class HomeFragment : Fragment() {
activity.themeManager.applyStatusBarTheme(activity)
requireContext().components.analytics.experiments.recordExposureEvent(FeatureId.HOME_PAGE)
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onCreateView",
)
return binding.root
}
@ -462,6 +476,9 @@ class HomeFragment : Fragment() {
@Suppress("LongMethod", "ComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// DO NOT ADD ANYTHING ABOVE THIS getProfilerTime CALL!
val profilerStartTime = requireComponents.core.engine.profiler?.getProfilerTime()
super.onViewCreated(view, savedInstanceState)
context?.metrics?.track(Event.HomeScreenDisplayed)
@ -532,6 +549,11 @@ class HomeFragment : Fragment() {
* the View action on the [TabsTrayDialogFragment] snackbar.*/
scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1))
}
// DO NOT MOVE ANYTHING BELOW THIS addMarker CALL!
requireComponents.core.engine.profiler?.addMarker(
MarkersFragmentLifecycleCallbacks.MARKER_NAME, profilerStartTime, "HomeFragment.onViewCreated",
)
}
private fun observeSearchEngineChanges() {
@ -884,7 +906,7 @@ class HomeFragment : Fragment() {
hideOnboardingIfNeeded()
nav(
R.id.homeFragment,
HomeFragmentDirections.actionGlobalCustomizationFragment()
HomeFragmentDirections.actionGlobalHomeSettingsFragment()
)
}
is HomeMenu.Item.SyncAccount -> {
@ -1157,11 +1179,6 @@ class HomeFragment : Fragment() {
?.isVisible = tabCount > 0
}
@SuppressLint("NotifyDataSetChanged")
private fun handleSwipedItemDeletionCancel() {
binding.sessionControlRecyclerView.adapter?.notifyDataSetChanged()
}
private fun getRecentTabs(components: Components): List<RecentTab> {
return if (components.settings.showRecentTabsFeature) {
components.core.store.state.asRecentTabs()

@ -17,9 +17,9 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.getFilteredStories
import org.mozilla.fenix.historymetadata.HistoryMetadataGroup
import org.mozilla.fenix.home.recenttabs.RecentTab
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.pocket.POCKET_STORIES_TO_SHOW_COUNT
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
/**
* The [Store] for holding the [HomeFragmentState] and applying [HomeFragmentAction]s.

@ -63,7 +63,6 @@ class HomeMenu(
private val syncDisconnectedBackgroundColor =
context.getColorFromAttr(R.attr.syncDisconnectedBackground)
private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
private val accountManager = FenixAccountManager(context)
// 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary.

@ -1,21 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home
import android.content.Context
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
class ImageViewTopCrop(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs) {
override fun setFrame(l: Int, t: Int, r: Int, b: Int): Boolean {
val scaleFactor = width / drawable.intrinsicWidth.toFloat()
val matrix = imageMatrix
matrix.setScale(scaleFactor, scaleFactor, 0f, 0f)
imageMatrix = matrix
return super.setFrame(l, t, r, b)
}
}

@ -17,8 +17,8 @@ import mozilla.components.service.pocket.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStoriesService
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories.SelectedPocketStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.sessioncontrol.viewholders.pocket.PocketRecommendedStoriesSelectedCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
/**
* [HomeFragmentStore] middleware reacting in response to Pocket related [Action]s.

@ -6,7 +6,6 @@ package org.mozilla.fenix.home.mozonline
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
@ -46,13 +45,13 @@ fun showPrivacyPopWindow(context: Context, activity: Activity) {
val builder = AlertDialog.Builder(activity)
.setPositiveButton(
context.getString(R.string.privacy_notice_positive_button),
DialogInterface.OnClickListener { _, _ ->
{ _, _ ->
context.settings().shouldShowPrivacyPopWindow = false
}
)
.setNeutralButton(
context.getString(R.string.privacy_notice_neutral_button),
DialogInterface.OnClickListener { _, _ -> exitProcess(0) }
{ _, _ -> exitProcess(0) }
)
.setTitle(context.getString(R.string.privacy_notice_title))
.setMessage(messageSpannable)

@ -2,7 +2,7 @@
* 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.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
import mozilla.components.service.pocket.PocketRecommendedStory

@ -2,7 +2,7 @@
* 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.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
/**
* Details about a selected Pocket recommended stories category.

@ -4,7 +4,7 @@
@file:Suppress("MagicNumber")
package org.mozilla.fenix.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
import android.net.Uri
import androidx.compose.foundation.background

@ -2,12 +2,10 @@
* 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.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
import androidx.annotation.VisibleForTesting
import androidx.navigation.NavController
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
import mozilla.components.lib.state.Store
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.BrowserDirection
@ -15,6 +13,8 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentStore
/**
* Contract for how all user interactions with the Pocket recommended stories feature are to be handled.
@ -32,7 +32,7 @@ interface PocketStoriesController {
*
* @param categoryClicked the just clicked [PocketRecommendedStoriesCategory].
*/
fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory): Unit
fun handleCategoryClick(categoryClicked: PocketRecommendedStoriesCategory)
/**
* Callback for when the user clicks on a specific story.

@ -2,7 +2,7 @@
* 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.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
import mozilla.components.service.pocket.PocketRecommendedStory

@ -2,7 +2,7 @@
* 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.home.sessioncontrol.viewholders.pocket
package org.mozilla.fenix.home.pocket
import android.view.View
import androidx.annotation.Dimension
@ -24,7 +24,7 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.lib.state.ext.observeAsComposableState
import mozilla.components.service.pocket.PocketRecommendedStory
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.HomeSectionHeader
import org.mozilla.fenix.compose.SectionHeader
import org.mozilla.fenix.home.HomeFragmentStore
import org.mozilla.fenix.theme.FirefoxTheme
@ -99,8 +99,8 @@ fun PocketStories(
}
}
Column(modifier = Modifier.padding(vertical = 44.dp)) {
HomeSectionHeader(
Column(modifier = Modifier.padding(top = 72.dp)) {
SectionHeader(
text = stringResource(R.string.pocket_stories_header_1),
modifier = Modifier
.fillMaxWidth()
@ -114,7 +114,7 @@ fun PocketStories(
Spacer(Modifier.height(24.dp))
HomeSectionHeader(
SectionHeader(
text = stringResource(R.string.pocket_stories_categories_header),
modifier = Modifier
.fillMaxWidth()

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

Loading…
Cancel
Save