Merge remote-tracking branch 'origin/fenix/116.0' into iceraven

pull/700/head
akliuxingyuan 11 months ago
commit b45e8cd324

@ -80,7 +80,6 @@ projects:
- support-rusterrors
- support-rusthttp
- support-rustlog
- support-sync-telemetry
- support-test
- support-test-libstate
- support-utils

@ -1 +1 @@
Subproject commit ac015fe2d5ef0700f93e40b62094f1cf79edcf86
Subproject commit 51fe6a7e4c1e4d01946aadddf21b788e8ce7fff7

@ -147,6 +147,36 @@ search-term-groups:
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."
search_extra_params:
description: A feature that provides a search engine name and a channel ID.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active."
search_name_channel_id:
type: json
description: The search engine name and the channel ID.
shopping-experience:
description: A feature that shows product review quality information.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, the shopping experience feature is shown to the user."
splash-screen:
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active."
maximum_duration_ms:
type: int
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
toolbar:
description: The searchbar/awesomebar that user uses to search.
hasExposure: true

@ -23,7 +23,7 @@ import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
android {
compileSdkVersion Config.compileSdkVersion
compileSdkVersion config.compileSdkVersion
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) {
@ -34,8 +34,8 @@ android {
defaultConfig {
applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion
targetSdkVersion Config.targetSdkVersion
minSdkVersion config.minSdkVersion
targetSdkVersion config.targetSdkVersion
versionCode 1
versionName Config.generateDebugVersionName()
vectorDrawables.useSupportLibrary = true
@ -246,8 +246,8 @@ android {
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lint {
@ -281,7 +281,7 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = FenixVersions.androidx_compose_compiler
kotlinCompilerExtensionVersion = Versions.compose_compiler
}
namespace 'org.mozilla.fenix'
@ -322,7 +322,9 @@ android.applicationVariants.all { variant ->
println("versionCode for $abi = $versionCodeOverride, isMozillaOnline = $isMozillaOnline")
output.versionNameOverride = versionName
if (versionName != null) {
output.versionNameOverride = versionName
}
output.versionCodeOverride = versionCodeOverride
}
} else if (gradle.hasProperty("localProperties.branchBuild.fenix.version")) {
@ -517,15 +519,14 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
dependencies {
implementation project(':browser-engine-gecko')
implementation FenixDependencies.kotlin_coroutines
implementation FenixDependencies.kotlin_coroutines_android
testImplementation FenixDependencies.kotlin_coroutines_test
implementation FenixDependencies.androidx_appcompat
implementation FenixDependencies.androidx_constraintlayout
implementation FenixDependencies.androidx_coordinatorlayout
implementation ComponentsDependencies.kotlin_coroutines
testImplementation ComponentsDependencies.testing_coroutines
implementation ComponentsDependencies.androidx_appcompat
implementation ComponentsDependencies.androidx_constraintlayout
implementation ComponentsDependencies.androidx_coordinatorlayout
implementation FenixDependencies.google_accompanist_drawablepainter
implementation FenixDependencies.sentry
implementation ComponentsDependencies.thirdparty_sentry_latest
implementation project(':compose-awesomebar')
implementation project(':compose-cfr')
@ -615,48 +616,49 @@ dependencies {
implementation project(':lib-state')
implementation project(':lib-dataprotect')
debugImplementation FenixDependencies.leakcanary
forkDebugImplementation FenixDependencies.leakcanary
debugImplementation FenixDependencies.androidx_compose_ui_tooling
debugImplementation ComponentsDependencies.leakcanary
forkDebugImplementation ComponentsDependencies.leakcanary
debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
implementation FenixDependencies.androidx_activity_compose
implementation ComponentsDependencies.androidx_activity_compose
implementation FenixDependencies.androidx_activity_ktx
implementation FenixDependencies.androidx_annotation
implementation FenixDependencies.androidx_compose_ui
implementation FenixDependencies.androidx_compose_ui_tooling_preview
implementation FenixDependencies.androidx_compose_foundation
implementation FenixDependencies.androidx_compose_material
implementation ComponentsDependencies.androidx_annotation
implementation ComponentsDependencies.androidx_compose_ui
implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
implementation ComponentsDependencies.androidx_compose_foundation
implementation ComponentsDependencies.androidx_compose_material
implementation FenixDependencies.androidx_legacy
implementation FenixDependencies.androidx_biometric
implementation ComponentsDependencies.androidx_biometric
implementation FenixDependencies.androidx_paging
implementation FenixDependencies.androidx_preference
implementation FenixDependencies.androidx_fragment
implementation ComponentsDependencies.androidx_preferences
implementation ComponentsDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation FenixDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_recyclerview
implementation FenixDependencies.androidx_lifecycle_common
implementation FenixDependencies.androidx_lifecycle_livedata
implementation FenixDependencies.androidx_lifecycle_process
implementation FenixDependencies.androidx_lifecycle_runtime
implementation FenixDependencies.androidx_lifecycle_viewmodel
implementation FenixDependencies.androidx_core
implementation FenixDependencies.androidx_core_ktx
implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx
implementation FenixDependencies.androidx_core_splashscreen
implementation FenixDependencies.androidx_transition
implementation FenixDependencies.androidx_work_ktx
implementation ComponentsDependencies.androidx_work_runtime
implementation FenixDependencies.androidx_datastore
implementation FenixDependencies.androidx_data_store_preferences
implementation ComponentsDependencies.androidx_data_store_preferences
implementation FenixDependencies.protobuf_javalite
implementation FenixDependencies.google_material
implementation ComponentsDependencies.google_material
androidTestImplementation FenixDependencies.uiautomator
androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
androidTestImplementation FenixDependencies.fastlane
// This Falcon version is added to maven central now required for Screengrab
androidTestImplementation FenixDependencies.falcon
androidTestImplementation FenixDependencies.androidx_compose_ui_test
androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
androidTestImplementation FenixDependencies.espresso_core, {
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
@ -670,32 +672,30 @@ dependencies {
exclude module: 'protobuf-lite'
}
androidTestImplementation FenixDependencies.androidx_test_core
androidTestImplementation ComponentsDependencies.androidx_test_core
androidTestImplementation FenixDependencies.espresso_idling_resources
androidTestImplementation FenixDependencies.espresso_intents
androidTestImplementation FenixDependencies.tools_test_runner
androidTestImplementation FenixDependencies.tools_test_rules
androidTestImplementation ComponentsDependencies.androidx_test_runner
androidTestImplementation ComponentsDependencies.androidx_test_rules
androidTestUtil FenixDependencies.orchestrator
androidTestImplementation FenixDependencies.espresso_core, {
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation FenixDependencies.androidx_junit
androidTestImplementation FenixDependencies.androidx_test_extensions
androidTestImplementation FenixDependencies.androidx_work_testing
androidTestImplementation ComponentsDependencies.androidx_test_junit
androidTestImplementation ComponentsDependencies.androidx_work_testing
androidTestImplementation FenixDependencies.androidx_benchmark_junit4
androidTestImplementation FenixDependencies.mockwebserver
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
testImplementation FenixDependencies.androidx_junit
testImplementation FenixDependencies.androidx_test_extensions
testImplementation FenixDependencies.androidx_work_testing
testImplementation (FenixDependencies.robolectric) {
testImplementation ComponentsDependencies.androidx_test_junit
testImplementation ComponentsDependencies.androidx_work_testing
testImplementation (ComponentsDependencies.testing_robolectric) {
exclude group: 'org.apache.maven'
}
testImplementation FenixDependencies.maven_ant_tasks
testImplementation ComponentsDependencies.testing_maven_ant_tasks
implementation project(':support-rusthttp')
androidTestImplementation FenixDependencies.mockk_android
@ -734,7 +734,7 @@ if (project.hasProperty("coverage")) {
}
jacoco {
toolVersion = FenixVersions.jacoco
toolVersion = Versions.jacoco
}
android.applicationVariants.all { variant ->

File diff suppressed because it is too large Load Diff

@ -320,6 +320,41 @@ features:
type: Map<String, Boolean>
default: {}
splash-screen:
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
variables:
enabled:
description: "If true, the feature is active."
type: Boolean
default: false
maximum_duration_ms:
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
type: Int
default: 0
shopping-experience:
description: A feature that shows product review quality information.
variables:
enabled:
description: if true, the shopping experience feature is shown to the user.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
search_extra_params:
description: A feature that provides a search engine name and a channel ID.
variables:
enabled:
description: If true, the feature is active.
type: Boolean
default: false
search_name_channel_id:
description: The search engine name and the channel ID.
type: Map<String, String>
default: {}
types:
objects: {}

@ -9,7 +9,7 @@
<a href="https://play.google.com/store/apps/details?id=org.mozilla.fenix">Mozilla Playstore link</a>
</p>
<p>
<a href="../resources/washington.pdf">PDF file</a>
<a href="../resources/pdfForm.pdf">PDF form file</a>
</p>
</h1>
</body>

@ -13,6 +13,7 @@ object Constants {
const val GOOGLE_PLAY_SERVICES = "com.android.vending"
const val GOOGLE_APPS_PHOTOS = "com.google.android.apps.photos"
const val GOOGLE_QUICK_SEARCH = "com.google.android.googlequicksearchbox"
const val GOOGLE_DOCS = "com.google.android.apps.docs"
const val YOUTUBE_APP = "com.google.android.youtube"
const val GMAIL_APP = "com.google.android.gm"
const val PHONE_APP = "com.android.dialer"

@ -82,6 +82,11 @@ interface FeatureSettingsHelper {
*/
var tabsTrayRewriteEnabled: Boolean
/**
* Enable or disable the Unified search feature.
*/
var isUnifiedSearchEnabled: Boolean
fun applyFlagUpdates()
fun resetAllFeatureFlags()

@ -17,7 +17,7 @@ import org.mozilla.fenix.utils.Settings
/**
* Helper for querying the status and modifying various features and settings in the application.
*/
class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
/**
* The current feature flags used inside the app before the tests start.
* These will be restored when the tests end.
@ -56,6 +56,8 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
false -> 0
}
}
override var isUnifiedSearchEnabled: Boolean by updatedFeatureFlags::isUnifiedSearchEnabled
override var isPocketEnabled: Boolean by updatedFeatureFlags::isPocketEnabled
override var isJumpBackInCFREnabled: Boolean by updatedFeatureFlags::isJumpBackInCFREnabled
override var isWallpaperOnboardingEnabled: Boolean by updatedFeatureFlags::isWallpaperOnboardingEnabled
@ -107,7 +109,7 @@ private data class FeatureFlags(
var isRecentlyVisitedFeatureEnabled: Boolean,
var isPWAsPromptEnabled: Boolean,
var isTCPCFREnabled: Boolean,
val isUnifiedSearchEnabled: Boolean,
var isUnifiedSearchEnabled: Boolean,
var isWallpaperOnboardingEnabled: Boolean,
var isDeleteSitePermissionsEnabled: Boolean,
var isCookieBannerReductionDialogEnabled: Boolean,

@ -55,6 +55,7 @@ class HomeActivityTestRule(
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
isUnifiedSearchEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -69,6 +70,7 @@ class HomeActivityTestRule(
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.isUnifiedSearchEnabled = isUnifiedSearchEnabled
}
/**
@ -124,6 +126,7 @@ class HomeActivityTestRule(
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,
isUnifiedSearchEnabled = false,
)
}
}
@ -157,6 +160,7 @@ class HomeActivityIntentTestRule internal constructor(
isRecentlyVisitedFeatureEnabled: Boolean = settings.historyMetadataUIFeature,
isPWAsPromptEnabled: Boolean = !settings.userKnowsAboutPwas,
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
isUnifiedSearchEnabled: Boolean = false,
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isCookieBannerReductionDialogEnabled: Boolean = !settings.userOptOutOfReEngageCookieBannerDialog,
@ -171,6 +175,7 @@ class HomeActivityIntentTestRule internal constructor(
this.isRecentlyVisitedFeatureEnabled = isRecentlyVisitedFeatureEnabled
this.isPWAsPromptEnabled = isPWAsPromptEnabled
this.isTCPCFREnabled = isTCPCFREnabled
this.isUnifiedSearchEnabled = isUnifiedSearchEnabled
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isCookieBannerReductionDialogEnabled = isCookieBannerReductionDialogEnabled
@ -258,6 +263,7 @@ class HomeActivityIntentTestRule internal constructor(
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
isUnifiedSearchEnabled: Boolean = false,
) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
@ -266,6 +272,7 @@ class HomeActivityIntentTestRule internal constructor(
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
isUnifiedSearchEnabled = isUnifiedSearchEnabled,
isWallpaperOnboardingEnabled = false,
isCookieBannerReductionDialogEnabled = false,
isOpenInAppBannerEnabled = false,

@ -4,8 +4,9 @@
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.espresso.Espresso.pressBack
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
@ -33,14 +34,11 @@ import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.searchScreen
/**
* Tests for verifying basic functionality of bookmarks
*/
class BookmarksTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
private val bookmarksFolderName = "New Folder"
@ -49,16 +47,19 @@ class BookmarksTest {
var url: String = "https://www.example.com"
}
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@get:Rule(order = 0)
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(isUnifiedSearchEnabled = true),
) { it.activity }
@Rule
@Rule(order = 1)
@JvmField
val retryTestRule = RetryTestRule(3)
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice = UiDevice.getInstance(getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
@ -235,7 +236,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickCopy {
verifyCopySnackBarText()
navigateUp()
@ -261,7 +262,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickShare {
verifyShareOverlay()
verifyShareBookmarkFavicon()
@ -281,7 +282,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickOpenInNewTab {
verifyTabTrayIsOpened()
verifyNormalModeSelected()
@ -376,7 +377,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickOpenInPrivateTab {
verifyTabTrayIsOpened()
verifyPrivateModeSelected()
@ -395,7 +396,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickDelete {
verifyDeleteSnackBarText()
verifyUndoDeleteSnackBarButton()
@ -414,7 +415,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickDelete {
verifyUndoDeleteSnackBarButton()
clickUndoDeleteButton()
@ -708,7 +709,7 @@ class BookmarksTest {
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2),
) {}
}.openThreeDotMenu(defaultWebPage.url) {
}.openThreeDotMenu(defaultWebPage.title) {
}.clickEdit {
clickDeleteInEditModeButton()
cancelDeletion()
@ -752,11 +753,17 @@ class BookmarksTest {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
verifyBookmarksSearchBarPosition(true)
clickOutsideTheSearchBar()
verifyBookmarksSearchBar(false)
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchSelectorButton()
verifySearchEngineIcon("Bookmarks")
verifySearchBarPlaceholder("Search bookmarks")
verifySearchBarPosition(true)
tapOutsideToDismissSearchBar()
verifySearchToolbar(false)
}
bookmarksMenu {
}.goBackToBrowserScreen {
}.openThreeDotMenu {
}.openSettings {
@ -769,11 +776,12 @@ class BookmarksTest {
browserScreen {
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
verifyBookmarksSearchBarPosition(false)
dismissBookmarksSearchBarUsingBackButton()
verifyBookmarksSearchBar(false)
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("Bookmarks")
verifySearchBarPosition(false)
pressBack()
verifySearchToolbar(false)
}
}
@ -795,15 +803,15 @@ class BookmarksTest {
createBookmark(secondWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
}.clickSearchButton {
// Search for a valid term
searchBookmarkedItem(firstWebPage.title)
verifySearchedBookmarkExists(firstWebPage.url.toString(), true)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
typeSearch(firstWebPage.title)
verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
// Search for invalid term
searchBookmarkedItem("Android")
verifySearchedBookmarkExists(firstWebPage.url.toString(), false)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
typeSearch("Android")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
}
}
@ -815,10 +823,9 @@ class BookmarksTest {
createBookmark(defaultWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
clickSearchButton()
verifyBookmarksSearchBar(true)
}
searchScreen {
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("Bookmarks")
startVoiceSearch()
}
}
@ -835,24 +842,28 @@ class BookmarksTest {
createBookmark(thirdWebPage.url)
}.openThreeDotMenu {
}.openBookmarks {
}.openThreeDotMenu(firstWebPage.url) {
}.openThreeDotMenu(firstWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(firstWebPage.title)
}.openThreeDotMenu(secondWebPage.url) {
}.openThreeDotMenu(secondWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(secondWebPage.title)
clickSearchButton()
searchBookmarkedItem("generic")
verifySearchedBookmarkExists(firstWebPage.url.toString(), false)
verifySearchedBookmarkExists(secondWebPage.url.toString(), false)
verifySearchedBookmarkExists(thirdWebPage.url.toString(), true)
dismissBookmarksSearchBar()
}.openThreeDotMenu(thirdWebPage.url) {
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString())
pressBack()
}
bookmarksMenu {
}.openThreeDotMenu(thirdWebPage.title) {
}.clickDelete {
verifyBookmarkIsDeleted(thirdWebPage.title)
clickSearchButton()
searchBookmarkedItem("generic")
verifySearchedBookmarkExists(thirdWebPage.url.toString(), false)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, thirdWebPage.url.toString())
}
}
}

@ -11,14 +11,12 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.helpers.TestHelper.verifySnackBarText
import org.mozilla.fenix.ui.robots.browserScreen
@ -91,7 +89,7 @@ class ComposeTabbedBrowsingTest {
verifyNoOpenTabsInNormalBrowsing()
}.openNewTab {
}.submitQuery(defaultWebPage.url.toString()) {
mDevice.waitForIdle()
verifyPageContent(defaultWebPage.content)
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyNormalBrowsingButtonIsSelected()
@ -150,36 +148,35 @@ class ComposeTabbedBrowsingTest {
}
}
@Ignore("Being converted in: https://bugzilla.mozilla.org/show_bug.cgi?id=1832617")
@Test
fun closeTabTest() {
// val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// closeTab()
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabRight("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabLeft("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
closeTab()
}
homeScreen {
verifyTabCounter("0")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
}
homeScreen {
verifyTabCounter("0")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
}
homeScreen {
verifyTabCounter("0")
}
}
@Test
@ -209,39 +206,36 @@ class ComposeTabbedBrowsingTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1829838")
// Try converting in: https://bugzilla.mozilla.org/show_bug.cgi?id=1832609
@Test
fun closePrivateTabTest() {
// val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// homeScreen { }.togglePrivateBrowsingMode()
// navigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// verifyCloseTabsButton("Test_Page_1")
// closeTab()
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabRight("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(genericURL.url) {
// }.openTabDrawer {
// verifyExistingOpenTabs("Test_Page_1")
// swipeTabLeft("Test_Page_1")
// }
// homeScreen {
// verifyTabCounter("0")
// }
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { }.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
closeTab()
}
homeScreen {
verifyTabCounter("0")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
swipeTabRight("Test_Page_1")
}
homeScreen {
verifyTabCounter("0")
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
swipeTabLeft("Test_Page_1")
}
homeScreen {
verifyTabCounter("0")
}
}
@Test
@ -251,14 +245,16 @@ class ComposeTabbedBrowsingTest {
homeScreen { }.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
verifyPageContent(genericURL.content)
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")
closeTab()
TestHelper.verifySnackBarText("Private tab closed")
TestHelper.clickSnackbarButton("UNDO")
verifySnackBarText("Private tab closed")
clickSnackbarButton("UNDO")
}
browserScreen {
verifyPageContent(genericURL.content)
verifyTabCounter("1")
}.openComposeTabDrawer(composeTestRule) {
verifyExistingOpenTabs("Test_Page_1")

@ -16,9 +16,11 @@ import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.ui.robots.clickContextMenuItem
import org.mozilla.fenix.ui.robots.clickPageObject
@ -267,7 +269,8 @@ class ContextMenusTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
waitForPageToLoad()
longClickPageObject(itemWithText("Wikipedia link"))
verifyLinkContextMenuItems("wikipedia.org".toUri(), false)
dismissContentContextMenu()
@ -278,4 +281,16 @@ class ContextMenusTest {
dismissContentContextMenu()
}
}
@Test
fun verifyContextOpenLinkInAppTest() {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
longClickPageObject(itemContainingText("Youtube link"))
clickContextMenuItem("Open link in external app")
assertYoutubeAppOpens()
}
}
}

@ -57,6 +57,7 @@ class CookieBannerReductionTest {
exitMenu()
browserScreen {
waitForPageToLoad()
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)
@ -107,6 +108,7 @@ class CookieBannerReductionTest {
exitMenu()
}
browserScreen {
waitForPageToLoad()
}.openThreeDotMenu {
}.refreshPage {
verifyCookieBannerExists(exists = false)

@ -12,16 +12,19 @@ import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.clickSnackbarButton
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.setNetworkEnabled
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
@ -39,7 +42,6 @@ class DownloadTest {
/* Remote test page managed by Mozilla Mobile QA team at https://github.com/mozilla-mobile/testapp */
private val downloadTestPage = "https://storage.googleapis.com/mobile_test_assets/test_app/downloads.html"
private var downloadFile: String = ""
private val pdfFileName = "washington.pdf"
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@ -65,6 +67,8 @@ class DownloadTest {
}
mockWebServer.shutdown()
setNetworkEnabled(enabled = true)
}
@Test
@ -83,6 +87,7 @@ class DownloadTest {
verifyPhotosAppOpens()
}
mDevice.pressBack()
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
@ -117,6 +122,7 @@ class DownloadTest {
notificationShade {
verifySystemNotificationExists("Download completed")
}
deleteDownloadedFileOnStorage(downloadFile)
}
@SmokeTest
@ -149,12 +155,13 @@ class DownloadTest {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}
deleteDownloadedFileOnStorage(downloadFile)
}
/* Verifies downloads in the Downloads Menu:
- downloads appear in the list
- deleting a download from device storage, removes it from the Downloads Menu too
*/
*/
@SmokeTest
@Test
fun manageDownloadsInDownloadsMenuTest() {
@ -205,37 +212,362 @@ class DownloadTest {
verifyPhotosAppOpens()
mDevice.pressBack()
}
deleteDownloadedFileOnStorage(downloadFile)
}
// Save PDF file from the share overlay
@SmokeTest
@Test
fun openPDFInBrowserTest() {
fun saveAndOpenPdfTest() {
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
downloadFile = "pdfForm.pdf"
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemContainingText("PDF file"))
verifyPageContent("Washington Crossing the Delaware")
clickPageObject(itemWithText("PDF form file"))
}.openThreeDotMenu {
}.clickShareButton {
}.clickSaveAsPDF {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}.clickOpen("application/pdf") {
assertExternalAppOpens(GOOGLE_DOCS)
}
deleteDownloadedFileOnStorage(downloadFile)
}
@SmokeTest
@Test
fun saveAndOpenPdfTest() {
fun deleteDownloadedFileTest() {
downloadFile = "smallZip.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
verifyDownloadedFileName(downloadFile)
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyDownloadedFileName(downloadFile)
deleteDownloadedItem(downloadFile)
verifyEmptyDownloadsList()
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun undoDeleteDownloadedFileTest() {
downloadFile = "smallZip.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
verifyDownloadedFileName(downloadFile)
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyDownloadedFileName(downloadFile)
deleteDownloadedItem(downloadFile)
clickSnackbarButton("UNDO")
verifyDownloadedFileName(downloadFile)
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun deleteMultipleDownloadedFilesTest() {
val firstDownloadedFile = "smallZip.zip"
val secondDownloadedFile = "textfile.txt"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(firstDownloadedFile) {
verifyDownloadPrompt(firstDownloadedFile)
}.clickDownload {
verifyDownloadedFileName(firstDownloadedFile)
}.closeCompletedDownloadPrompt {
}.clickDownloadLink(secondDownloadedFile) {
verifyDownloadPrompt(secondDownloadedFile)
}.clickDownload {
verifyDownloadedFileName(secondDownloadedFile)
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyDownloadedFileName(firstDownloadedFile)
verifyDownloadedFileName(secondDownloadedFile)
longClickDownloadedItem(firstDownloadedFile)
selectDownloadedItem(secondDownloadedFile)
openMultiSelectMoreOptionsMenu()
clickMultiSelectRemoveButton()
verifyEmptyDownloadsList()
}
deleteDownloadedFileOnStorage(firstDownloadedFile)
deleteDownloadedFileOnStorage(secondDownloadedFile)
}
@Test
fun undoDeleteMultipleDownloadedFilesTest() {
val firstDownloadedFile = "smallZip.zip"
val secondDownloadedFile = "textfile.txt"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(firstDownloadedFile) {
verifyDownloadPrompt(firstDownloadedFile)
}.clickDownload {
verifyDownloadedFileName(firstDownloadedFile)
}.closeCompletedDownloadPrompt {
}.clickDownloadLink(secondDownloadedFile) {
verifyDownloadPrompt(secondDownloadedFile)
}.clickDownload {
verifyDownloadedFileName(secondDownloadedFile)
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyDownloadedFileName(firstDownloadedFile)
verifyDownloadedFileName(secondDownloadedFile)
longClickDownloadedItem(firstDownloadedFile)
selectDownloadedItem(secondDownloadedFile)
openMultiSelectMoreOptionsMenu()
clickMultiSelectRemoveButton()
clickSnackbarButton("UNDO")
verifyDownloadedFileName(firstDownloadedFile)
verifyDownloadedFileName(secondDownloadedFile)
}
deleteDownloadedFileOnStorage(firstDownloadedFile)
deleteDownloadedFileOnStorage(secondDownloadedFile)
}
@Test
fun systemNotificationCantBeDismissedWhileDownloadingTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
browserScreen {
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
swipeDownloadNotification("Left", false)
verifySystemNotificationExists("Firefox Fenix")
}.closeNotificationTray {
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
swipeDownloadNotification("Right", false)
verifySystemNotificationExists("Firefox Fenix")
clickDownloadNotificationControlButton("CANCEL")
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun systemNotificationCantBeDismissedWhileDownloadIsPausedTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
browserScreen {
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
clickDownloadNotificationControlButton("PAUSE")
swipeDownloadNotification("Left", false)
verifySystemNotificationExists("Firefox Fenix")
}.closeNotificationTray {
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
expandNotificationMessage()
swipeDownloadNotification("Right", false)
verifySystemNotificationExists("Firefox Fenix")
clickDownloadNotificationControlButton("CANCEL")
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun notificationCanBeDismissedIfDownloadIsInterruptedTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
setNetworkEnabled(enabled = false)
browserScreen {
}.openNotificationShade {
verifySystemNotificationExists("Download failed")
expandNotificationMessage()
swipeDownloadNotification("Left", true)
verifySystemNotificationDoesNotExist("Firefox Fenix")
}.closeNotificationTray {
}
downloadRobot {
}.closeDownloadPrompt {
verifyDownloadPromptIsDismissed()
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun notificationCanBeDismissedIfDownloadIsCompletedTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "smallZip.zip"
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
browserScreen {
}.openNotificationShade {
verifySystemNotificationExists("Download completed")
swipeDownloadNotification("Left", true, false)
verifySystemNotificationDoesNotExist("Firefox Fenix")
}.closeNotificationTray {
}
downloadRobot {
}.closeDownloadPrompt {
verifyDownloadPromptIsDismissed()
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun stayInPrivateBrowsingPromptTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
browserScreen {
}.openTabDrawer {
closeTab()
}
browserScreen {
verifyCancelPrivateDownloadsPrompt("1")
clickStayInPrivateBrowsingPromptButton()
}.openNotificationShade {
verifySystemNotificationExists("Firefox Fenix")
}
deleteDownloadedFileOnStorage(downloadFile)
}
@Test
fun cancelActiveDownloadsFromPrivateBrowsingPromptTest() {
// Clear the "Firefox Fenix default browser notification"
notificationShade {
cancelAllShownNotifications()
}
downloadFile = "1GB.zip"
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadTestPage.toUri()) {
waitForPageToLoad()
}.clickDownloadLink(downloadFile) {
verifyDownloadPrompt(downloadFile)
}.clickDownload {
}
browserScreen {
}.openTabDrawer {
closeTab()
}
browserScreen {
verifyCancelPrivateDownloadsPrompt("1")
clickCancelPrivateDownloadsPromptButton()
}.openNotificationShade {
verifySystemNotificationDoesNotExist("Firefox Fenix")
}
deleteDownloadedFileOnStorage(downloadFile)
}
// Save edited PDF file from the share overlay
@Test
fun saveEditedPdfTest() {
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
downloadFile = "pdfForm.pdf"
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
waitForPageToLoad()
fillPdfForm("Firefox")
}.openThreeDotMenu {
}.clickShareButton {
}.clickSaveAsPDF {
verifyDownloadPrompt(pdfFileName)
verifyDownloadPrompt("pdfForm.pdf")
}.clickDownload {
}.clickOpen("application/pdf") {
assertExternalAppOpens("com.google.android.apps.docs")
assertExternalAppOpens(GOOGLE_DOCS)
}
deleteDownloadedFileOnStorage(downloadFile)
}
}

@ -5,7 +5,9 @@
package org.mozilla.fenix.ui
import android.content.Context
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.Espresso.pressBack
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
@ -20,11 +22,13 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.historyMenu
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.multipleSelectionToolbar
@ -35,12 +39,14 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*
*/
class HistoryTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
val activityTestRule =
AndroidComposeTestRule(
HomeActivityIntentTestRule.withDefaultSettingsOverrides(isUnifiedSearchEnabled = true),
) { it.activity }
@Before
fun setUp() {
@ -156,7 +162,6 @@ class HistoryTest {
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1),
) {
clickDeleteAllHistoryButton()
}
verifyDeleteConfirmationMessage()
selectEverythingOption()
@ -332,27 +337,124 @@ class HistoryTest {
}
}
// This test verifies the Recently Closed Tabs List and items
@Test
fun verifyRecentlyClosedTabsListTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
fun verifySearchHistoryViewTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchSelectorButton()
verifySearchEngineIcon("history")
verifySearchBarPlaceholder("Search history")
verifySearchBarPosition(true)
tapOutsideToDismissSearchBar()
verifySearchToolbar(false)
exitMenu()
}
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
) {
verifyRecentlyClosedTabsMenuView()
}
verifyRecentlyClosedTabsPageTitle("Test_Page_1")
verifyRecentlyClosedTabsUrl(website.url)
}.openThreeDotMenu {
}.openSettings {
}.openCustomizeSubMenu {
clickTopToolbarToggle()
}
exitMenu()
browserScreen {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchView()
verifySearchToolbar(true)
verifySearchBarPosition(false)
pressBack()
}
historyMenu {
verifyHistoryMenuView()
}
}
@Test
fun verifyVoiceSearchInHistoryTest() {
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
verifySearchToolbar(true)
verifySearchEngineIcon("history")
startVoiceSearch()
}
}
@Test
fun verifySearchForHistoryItemsTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getHTMLControlsFormAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
}.openThreeDotMenu {
}.openHistory {
}.clickSearchButton {
// Search for a valid term
typeSearch(firstWebPage.title)
verifySearchEngineSuggestionResults(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
clickClearButton()
// Search for invalid term
typeSearch("Android")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
}
}
@Test
fun verifyDeletedHistoryItemsCanNotBeSearchedTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
val thirdWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
verifyPageContent(firstWebPage.content)
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebPage.url) {
verifyPageContent(secondWebPage.content)
}
navigationToolbar {
}.enterURLAndEnterToBrowser(thirdWebPage.url) {
verifyPageContent(thirdWebPage.content)
}.openThreeDotMenu {
}.openHistory {
verifyHistoryListExists()
clickDeleteHistoryButton(firstWebPage.title)
verifyHistoryItemExists(false, firstWebPage.title)
clickDeleteHistoryButton(secondWebPage.title)
verifyHistoryItemExists(false, secondWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, firstWebPage.url.toString())
verifyNoSuggestionsAreDisplayed(activityTestRule, secondWebPage.url.toString())
verifySearchEngineSuggestionResults(activityTestRule, thirdWebPage.url.toString())
pressBack()
}
historyMenu {
clickDeleteHistoryButton(thirdWebPage.title)
verifyHistoryItemExists(false, firstWebPage.title)
}.clickSearchButton {
// Search for a valid term
typeSearch("generic")
verifyNoSuggestionsAreDisplayed(activityTestRule, thirdWebPage.url.toString())
}
}
}

@ -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.helpers.AndroidAssetDispatcher
@ -29,8 +28,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/
class HomeScreenTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
private lateinit var firstPocketStoryPublisher: String
@ -58,11 +55,9 @@ class HomeScreenTest {
mockWebServer.shutdown()
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815275")
@Test
fun homeScreenItemsTest() {
homeScreen { }.dismissOnboarding()
homeScreen {}.dismissOnboarding()
homeScreen {
verifyHomeWordmark()
verifyHomePrivateBrowsingButton()
@ -72,12 +67,8 @@ class HomeScreenTest {
verifyCollectionsHeader()
verifyNoCollectionsText()
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7)
verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8)
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
verifyThoughtProvokingStories(true)
verifyStoriesByTopicItems()
verifyPoweredByPocket(activityTestRule)
verifyCustomizeHomepageButton(true)
verifyNavigationToolbar()
verifyDefaultSearchEngine("Google")
@ -149,7 +140,6 @@ class HomeScreenTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1815276")
@Test
fun verifyPocketHomepageStoriesTest() {
activityTestRule.activityRule.applySettingsExceptions {
@ -163,11 +153,13 @@ class HomeScreenTest {
homeScreen {
verifyThoughtProvokingStories(true)
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyPocketRecommendedStoriesItems(activityTestRule, 1, 3, 4, 5, 6, 7)
verifyPocketSponsoredStoriesItems(activityTestRule, 2, 8)
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
verifyPocketRecommendedStoriesItems()
// Sponsored Pocket stories are only advertised for a limited time.
// See also known issue https://bugzilla.mozilla.org/show_bug.cgi?id=1828629
// verifyPocketSponsoredStoriesItems(2, 8)
verifyDiscoverMoreStoriesButton()
verifyStoriesByTopic(true)
verifyPoweredByPocket()
}.openThreeDotMenu {
}.openCustomizeHome {
clickPocketButton()
@ -177,7 +169,6 @@ class HomeScreenTest {
}
}
@Ignore("Failing, see: https://bugzilla.mozilla.org/show_bug.cgi?id=1821016")
@Test
fun openPocketStoryItemTest() {
activityTestRule.activityRule.applySettingsExceptions {
@ -197,7 +188,6 @@ class HomeScreenTest {
}
}
@Ignore("Failed, see: https://github.com/mozilla-mobile/fenix/issues/28098")
@Test
fun openPocketDiscoverMoreTest() {
activityTestRule.activityRule.applySettingsExceptions {
@ -210,9 +200,8 @@ class HomeScreenTest {
homeScreen {
scrollToPocketProvokingStories()
swipePocketProvokingStories()
verifyDiscoverMoreStoriesButton(activityTestRule, 9)
}.clickPocketDiscoverMoreButton(activityTestRule, 9) {
verifyDiscoverMoreStoriesButton()
}.clickPocketDiscoverMoreButton {
verifyUrl("getpocket.com/explore")
}
}
@ -245,7 +234,7 @@ class HomeScreenTest {
}.dismissOnboarding()
homeScreen {
verifyPoweredByPocket(activityTestRule)
verifyPoweredByPocket()
}.clickPocketLearnMoreLink(activityTestRule) {
verifyUrl("mozilla.org/en-US/firefox/pocket")
}

@ -370,6 +370,7 @@ class LoginsTest {
}
}
@Ignore("https://bugzilla.mozilla.org/show_bug.cgi?id=1840561")
@Test
fun verifyLoginWithoutPasswordCanNotBeSavedTest() {
val loginPage = "https://mozilla-mobile.github.io/testapp/loginForm"

@ -33,8 +33,6 @@ import org.mozilla.fenix.ui.robots.notificationShade
* Note: this test only verifies media notifications, not media itself
*/
class MediaNotificationTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
private lateinit var mDevice: UiDevice
@ -91,7 +89,7 @@ class MediaNotificationTest {
mDevice.openNotification()
notificationShade {
verifySystemNotificationGone(videoTestPage.title)
verifySystemNotificationDoesNotExist(videoTestPage.title)
}
// close notification shade before the next test
@ -125,7 +123,7 @@ class MediaNotificationTest {
mDevice.openNotification()
notificationShade {
verifySystemNotificationGone(audioTestPage.title)
verifySystemNotificationDoesNotExist(audioTestPage.title)
}
// close notification shade before the next test
@ -162,7 +160,7 @@ class MediaNotificationTest {
mDevice.openNotification()
notificationShade {
verifySystemNotificationGone("A site is playing media")
verifySystemNotificationDoesNotExist("A site is playing media")
}
// close notification shade before and go back to regular mode before the next test

@ -37,7 +37,6 @@ class NavigationToolbarTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()
@ -194,7 +193,7 @@ class NavigationToolbarTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyFindInPageButton()
@ -202,7 +201,7 @@ class NavigationToolbarTest {
verifyFindInPageNextButton()
verifyFindInPagePrevButton()
verifyFindInPageCloseButton()
enterFindInPageQuery("o")
enterFindInPageQuery("l")
verifyFindNextInPageResult("1/2")
clickFindInPageNextButton()
verifyFindNextInPageResult("2/2")

@ -0,0 +1,91 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_DOCS
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.navigationToolbar
class PDFViewerTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = HomeActivityIntentTestRule.withDefaultSettingsOverrides()
@Before
fun setUp() {
mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
@SmokeTest
@Test
fun openPDFInBrowserTest() {
val genericURL =
TestAssetHelper.getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemContainingText("PDF form file"))
verifyPageContent("Washington Crossing the Delaware")
}
}
@Test
fun pdfViewerOpenInAppTest() {
val genericURL = getGenericAsset(mockWebServer, 3)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF form file"))
verifyPDFReaderToolbarItems()
clickPageObject(itemWithResIdAndText("openInApp", "Open in app"))
assertExternalAppOpens(GOOGLE_DOCS)
}
}
// Download PDF file using the download toolbar button
@Test
fun pdfViewerDownloadButtonTest() {
val genericURL = getGenericAsset(mockWebServer, 3)
val downloadFile = "pdfForm.pdf"
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF form file"))
}.clickDownloadPDFButton {
verifyDownloadedFileName(downloadFile)
}.clickOpen("application/pdf") {
assertExternalAppOpens(GOOGLE_DOCS)
}
deleteDownloadedFileOnStorage(downloadFile)
}
}

@ -56,6 +56,7 @@ class PwaTest {
}
}
@Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1807275")
@SmokeTest
@Test
fun emailLinkPWATest() {

@ -0,0 +1,291 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu
import androidx.test.espresso.intent.Intents
import okhttp3.mockwebserver.MockWebServer
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.customannotations.SmokeTest
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.getGenericAsset
import org.mozilla.fenix.helpers.TestHelper.longTapSelectItem
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.registerAndCleanupIdlingResources
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
/**
* Tests for verifying basic functionality of recently closed tabs history
*
*/
class RecentlyClosedTabsTest {
private lateinit var mockWebServer: MockWebServer
@get:Rule
val activityTestRule = AndroidComposeTestRule(
HomeActivityTestRule.withDefaultSettingsOverrides(
tabsTrayRewriteEnabled = true,
),
) { it.activity }
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
dispatcher = AndroidAssetDispatcher()
start()
}
Intents.init()
}
@After
fun tearDown() {
mockWebServer.shutdown()
}
// This test verifies the Recently Closed Tabs List and items
@Test
fun verifyRecentlyClosedTabsListTest() {
val website = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(
activityTestRule.activity.findViewById(R.id.recently_closed_list),
1,
),
) {
verifyRecentlyClosedTabsMenuView()
}
verifyRecentlyClosedTabsPageTitle("Test_Page_1")
verifyRecentlyClosedTabsUrl(website.url)
}
}
// Verifies that a recently closed item is properly opened
@SmokeTest
@Test
fun openRecentlyClosedItemTest() {
val website = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
) {
verifyRecentlyClosedTabsMenuView()
}
}.clickRecentlyClosedItem("Test_Page_1") {
verifyUrl(website.url.toString())
}
}
// Verifies that tapping the "x" button removes a recently closed item from the list
@SmokeTest
@Test
fun deleteRecentlyClosedTabsItemTest() {
val website = getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openComposeTabDrawer(activityTestRule) {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
) {
verifyRecentlyClosedTabsMenuView()
}
clickDeleteRecentlyClosedTabs()
verifyEmptyRecentlyClosedTabsList()
}
}
@Test
fun openInNewTabRecentlyClosedTabsTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
longTapSelectItem(firstPage.url)
longTapSelectItem(secondPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}.clickOpenInNewTab(activityTestRule) {
// URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
browserScreen {
verifyPageContent(secondPage.content)
verifyUrl(secondPage.url.toString())
}.openComposeTabDrawer(activityTestRule) {
verifyNormalBrowsingButtonIsSelected(true)
verifyExistingOpenTabs(firstPage.title, secondPage.title)
}
}
}
@Test
fun openInPrivateTabRecentlyClosedTabsTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
longTapSelectItem(firstPage.url)
longTapSelectItem(secondPage.url)
openActionBarOverflowOrOptionsMenu(activityTestRule.activity)
}.clickOpenInPrivateTab(activityTestRule) {
// URL verification to be removed once https://bugzilla.mozilla.org/show_bug.cgi?id=1839179 is fixed.
browserScreen {
verifyPageContent(secondPage.content)
verifyUrl(secondPage.url.toString())
}.openComposeTabDrawer(activityTestRule) {
verifyPrivateBrowsingButtonIsSelected(true)
verifyExistingOpenTabs(firstPage.title, secondPage.title)
}
}
}
@Test
fun shareMultipleRecentlyClosedTabsTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = getGenericAsset(mockWebServer, 2)
val sharingApp = "Gmail"
val urlString = "${firstPage.url}\n\n${secondPage.url}"
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
longTapSelectItem(firstPage.url)
longTapSelectItem(secondPage.url)
}.clickShare {
verifyShareTabsOverlay(firstPage.title, secondPage.title)
verifySharingWithSelectedApp(sharingApp, urlString, "${firstPage.title}, ${secondPage.title}")
}
}
@Test
fun privateBrowsingNotSavedInRecentlyClosedTabsTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = getGenericAsset(mockWebServer, 2)
homeScreen {}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
verifyEmptyRecentlyClosedTabsList()
}
}
@Test
fun deleteHistoryClearsRecentlyClosedTabsListTest() {
val firstPage = getGenericAsset(mockWebServer, 1)
val secondPage = getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstPage.url) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openNewTab {
}.submitQuery(secondPage.url.toString()) {
waitForPageToLoad()
}.openComposeTabDrawer(activityTestRule) {
}.openThreeDotMenu {
}.closeAllTabs {
}.openThreeDotMenu {
}.openHistory {
}.openRecentlyClosedTabs {
waitForListToExist()
}.goBackToHistoryMenu {
clickDeleteAllHistoryButton()
selectEverythingOption()
confirmDeleteAllHistory()
verifyEmptyHistoryView()
}.openRecentlyClosedTabs {
verifyEmptyRecentlyClosedTabsList()
}
}
}

@ -87,7 +87,7 @@ class SearchTest {
homeScreen {
}.openSearch {
verifySearchView()
verifyBrowserToolbar()
verifySearchToolbar(true)
verifyScanButton()
verifySearchEngineButton()
}

@ -25,8 +25,6 @@ import org.mozilla.fenix.ui.robots.homeScreen
*/
class SettingsAboutTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer

@ -7,14 +7,12 @@ package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import mozilla.components.concept.engine.utils.EngineReleaseChannel
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.customannotations.SmokeTest
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
@ -22,7 +20,6 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.runWithCondition
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
@ -33,8 +30,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*/
class SettingsAdvancedTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@ -79,108 +74,80 @@ class SettingsAdvancedTest {
@SmokeTest
@Test
fun verifyOpenLinkInAppViewTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
}
@SmokeTest
@Test
fun verifyOpenLinkInAppViewInPrivateBrowsingTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
}
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
}
}
// Assumes Youtube is installed and enabled
@Test
fun neverOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
// Assumes Youtube is installed and enabled
@Test
fun privateBrowsingNeverOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
}
@ -188,46 +155,39 @@ class SettingsAdvancedTest {
@SmokeTest
@Test
fun askBeforeOpeningLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyOpenLinkInAnotherAppPrompt()
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyOpenLinkInAnotherAppPrompt()
clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyOpenLinkInAnotherAppPrompt()
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyOpenLinkInAnotherAppPrompt()
clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
@ -235,84 +195,70 @@ class SettingsAdvancedTest {
@SmokeTest
@Test
fun privateBrowsingAskBeforeOpeningLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyPrivateOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Ask before opening")
verifySelectedOpenLinksInAppOption("Ask before opening")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Ask before opening")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
clickPageObject(itemWithResIdAndText("android:id/button2", "CANCEL"))
waitForPageToLoad()
verifyUrl("youtube.com")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
verifyPrivateBrowsingOpenLinkInAnotherAppPrompt("youtube.com")
clickPageObject(itemWithResIdAndText("android:id/button1", "OPEN"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}
// Assumes Youtube is installed and enabled
@Test
fun alwaysOpenLinkInAppTest() {
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
) {
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Always")
verifySelectedOpenLinksInAppOption("Always")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Always")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
val defaultWebPage = TestAssetHelper.getExternalLinksAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
verifyOpenLinksInAppsButton()
verifySettingsOptionSummary("Open links in apps", "Never")
}.openOpenLinksInAppsMenu {
verifyOpenLinksInAppsView("Never")
clickOpenLinkInAppOption("Always")
verifySelectedOpenLinksInAppOption("Always")
}.goBack {
verifySettingsOptionSummary("Open links in apps", "Always")
}
exitMenu()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
clickPageObject(itemContainingText("Youtube link"))
mDevice.waitForIdle()
assertYoutubeAppOpens()
}
}

@ -21,6 +21,7 @@ import org.mozilla.fenix.helpers.MatcherHelper
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.getStorageTestAsset
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadedFileOnStorage
import org.mozilla.fenix.helpers.TestHelper.exitMenu
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.restartApp
@ -35,7 +36,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
*
*/
class SettingsDeleteBrowsingDataOnQuitTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
@get:Rule
@ -182,6 +182,7 @@ class SettingsDeleteBrowsingDataOnQuitTest {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}
deleteDownloadedFileOnStorage("smallZip.zip")
}
@SmokeTest

@ -33,7 +33,6 @@ import org.mozilla.fenix.ui.robots.settingsScreen
*/
class SettingsDeleteBrowsingDataTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
@get:Rule

@ -22,8 +22,6 @@ import org.mozilla.fenix.ui.robots.homeScreen
*/
class SettingsDeveloperToolsTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer

@ -35,7 +35,6 @@ import java.util.Locale
*
*/
class SettingsGeneralTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
@get:Rule

@ -13,7 +13,10 @@ import org.junit.Rule
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.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade
/**
* Tests for verifying the the privacy and security section of the Settings menu
@ -21,8 +24,6 @@ import org.mozilla.fenix.ui.robots.homeScreen
*/
class SettingsPrivacyTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
@ -144,4 +145,40 @@ class SettingsPrivacyTest {
verifySitePermissionOption("Exceptions")
}
}
@Test
fun verifyNotificationsSettingsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// Clear all existing notifications
notificationShade {
mDevice.openNotification()
clearNotifications()
}
homeScreen {
}.togglePrivateBrowsingMode()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openNotificationShade {
verifySystemNotificationExists("Close private tabs")
}.closeNotificationTray {
}.openThreeDotMenu {
}.openSettings {
verifySettingsOptionSummary("Notifications", "Allowed")
}.openSettingsSubMenuNotifications {
verifyAllSystemNotificationsToggleState(true)
verifyPrivateBrowsingSystemNotificationsToggleState(true)
clickPrivateBrowsingSystemNotificationsToggle()
verifyPrivateBrowsingSystemNotificationsToggleState(false)
clickAllSystemNotificationsToggle()
verifyAllSystemNotificationsToggleState(false)
}.goBack {
verifySettingsOptionSummary("Notifications", "Not allowed")
}.goBackToBrowser {
}.openNotificationShade {
verifySystemNotificationDoesNotExist("Close private tabs")
}
}
}

@ -21,8 +21,6 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
*/
class SettingsSyncTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer

@ -26,7 +26,6 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.assertYoutubeAppOpens
@ -94,7 +93,7 @@ class SmokeTest {
- editing the url bar
- the tab drawer button
- opening a new search and dismissing the nav bar
*/
*/
@Test
fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -149,54 +148,6 @@ class SmokeTest {
}
}
// Verifies that a recently closed item is properly opened
@Test
fun openRecentlyClosedItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
) {
verifyRecentlyClosedTabsMenuView()
}
}.clickRecentlyClosedItem("Test_Page_1") {
verifyUrl(website.url.toString())
}
}
// Verifies that tapping the "x" button removes a recently closed item from the list
@Test
fun deleteRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
registerAndCleanupIdlingResources(
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1),
) {
verifyRecentlyClosedTabsMenuView()
}
clickDeleteRecentlyClosedTabs()
verifyEmptyRecentlyClosedTabsList()
}
}
// Verifies that deleting a Bookmarks folder also removes the item from inside it.
@Test
fun deleteNonEmptyBookmarkFolderTest() {

@ -42,7 +42,6 @@ class TabbedBrowsingTest {
private lateinit var mDevice: UiDevice
private lateinit var mockWebServer: MockWebServer
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@get:Rule
val activityTestRule = HomeActivityTestRule.withDefaultSettingsOverrides()

@ -14,6 +14,7 @@ import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithText
import org.mozilla.fenix.helpers.RetryTestRule
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickContextMenuItem
import org.mozilla.fenix.ui.robots.clickPageObject
import org.mozilla.fenix.ui.robots.homeScreen
@ -21,6 +22,7 @@ import org.mozilla.fenix.ui.robots.longClickPageObject
import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.openEditURLView
import org.mozilla.fenix.ui.robots.searchScreen
import org.mozilla.fenix.ui.robots.shareOverlay
class TextSelectionTest {
private lateinit var mDevice: UiDevice
@ -146,7 +148,7 @@ class TextSelectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Select all")
clickContextMenuItem("Copy")
@ -158,7 +160,7 @@ class TextSelectionTest {
clickClearButton()
longClickToolbar()
clickPasteText()
verifyTypedToolbarText("Washington Crossing the Delaware Wikipedia link")
verifyTypedToolbarText("Washington Crossing the Delaware Wikipedia linkName: Android")
}
}
@ -170,7 +172,7 @@ class TextSelectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Copy")
}.openNavigationToolbar {
@ -193,7 +195,7 @@ class TextSelectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
longClickPageObject(itemContainingText("Crossing"))
}.clickShareSelectedText {
verifyAndroidShareLayout()
@ -208,7 +210,7 @@ class TextSelectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Search")
verifyTabCounter("2")
@ -227,11 +229,98 @@ class TextSelectionTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
clickPageObject(itemWithText("PDF file"))
clickPageObject(itemWithText("PDF form file"))
longClickPageObject(itemContainingText("Crossing"))
clickContextMenuItem("Private Search")
verifyTabCounter("2")
verifyUrl("google")
}
}
@Test
fun verifyUrlBarTextSelectionOptionsTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openNavigationToolbar {
longClickEditModeToolbar()
verifyTextSelectionOptions("Open", "Cut", "Copy", "Share")
}
}
@Test
fun copyUrlBarTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openNavigationToolbar {
longClickEditModeToolbar()
clickContextMenuItem("Copy")
clickClearToolbarButton()
verifyToolbarIsEmpty()
longClickEditModeToolbar()
clickContextMenuItem("Paste")
verifyUrl(genericURL.url.toString())
}
}
@Test
fun cutUrlBarTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openNavigationToolbar {
longClickEditModeToolbar()
clickContextMenuItem("Cut")
verifyToolbarIsEmpty()
longClickEditModeToolbar()
clickContextMenuItem("Paste")
verifyUrl(genericURL.url.toString())
}
}
@Test
fun shareUrlBarTextTest() {
val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(genericURL.url) {
}.openNavigationToolbar {
longClickEditModeToolbar()
clickContextMenuItem("Share")
}
shareOverlay {
verifyAndroidShareLayout()
}
}
@Test
fun urlBarQuickActionsTest() {
val firstWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebsite = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebsite.url) {
longClickToolbar()
clickContextMenuItem("Copy")
}
navigationToolbar {
}.enterURLAndEnterToBrowser(secondWebsite.url) {
longClickToolbar()
clickContextMenuItem("Paste")
}
searchScreen {
verifyTypedToolbarText(firstWebsite.url.toString())
}.dismissSearchBar {
}
browserScreen {
verifyUrl(secondWebsite.url.toString())
longClickToolbar()
clickContextMenuItem("Paste & Go")
verifyUrl(firstWebsite.url.toString())
}
}
}

@ -26,8 +26,6 @@ import org.mozilla.fenix.ui.robots.longClickPageObject
*/
class ThreeDotMenuMainTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
private lateinit var mockWebServer: MockWebServer
@get:Rule

@ -0,0 +1,19 @@
# Espresso/UI Automator Tests on All Channels
When writing Espresso/UI Automator tests, by default, the tests are expected to run on all channels unless otherwise targeted. The provided code snippet below demonstrates a conditional check before running the tests on specific channels.
```
runWithCondition(
// Returns the GeckoView channel set for the current version, if a feature is limited to Nightly or Beta.
// Once this feature lands in RC we should remove the wrapper.
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.NIGHTLY ||
activityIntentTestRule.activity.components.core.engine.version.releaseChannel == EngineReleaseChannel.BETA,
)
```
The code uses the `runWithCondition()` function to determine the appropriate channel for the test. It checks if the current version's release channel is either Nightly or Beta using the `activityIntentTestRule.activity.components.core.engine.version.releaseChannel` property.
If the release channel is Nightly or Beta, the test is executed within the `runWithCondition()` block. However, once the feature under test lands in the Release Candidate (RC) channel, we suggest removing the wrapper and allowing the tests to run without any channel-specific condition.
This approach ensures that the tests are executed on all channels during the development and testing phase. However, when the feature stabilizes and reaches the RC channel, the conditional check can be removed to ensure the tests run consistently across all channels.
Please note that the actual implementation of the tests and their behavior may vary depending on the specific testing framework, project structure, and requirements. The provided code snippet serves as an example to showcase the concept of targeting specific channels during test execution.

@ -12,11 +12,11 @@ import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
@ -32,12 +32,9 @@ import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithDescriptionExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdAndTextExists
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
@ -247,59 +244,6 @@ class BookmarksRobot {
fun clickDeleteInEditModeButton() = deleteInEditModeButton().click()
fun clickSearchButton() = itemWithResId("$packageName:id/bookmark_search").click()
fun verifyBookmarksSearchBarPosition(defaultPosition: Boolean) {
onView(withId(R.id.toolbar))
.check(
if (defaultPosition) {
PositionAssertions.isCompletelyBelow(withId(R.id.pill_wrapper_divider))
} else {
PositionAssertions.isCompletelyAbove(withId(R.id.pill_wrapper_divider))
},
)
}
fun clickOutsideTheSearchBar() {
itemWithResId("$packageName:id/search_wrapper").click()
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime)
}
fun dismissBookmarksSearchBarUsingBackButton() {
for (i in 1..RETRY_COUNT) {
try {
mDevice.pressBack()
assertTrue(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime),
)
break
} catch (e: AssertionError) {
if (i == RETRY_COUNT) {
throw e
}
}
}
}
fun verifyBookmarksSearchBar(exists: Boolean) {
assertItemWithResIdExists(
itemWithResId("$packageName:id/toolbar"),
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_icon"),
exists = exists,
)
assertItemWithResIdAndTextExists(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
itemContainingText(getStringResource(R.string.bookmark_search)),
exists = exists,
)
assertItemWithDescriptionExists(
itemWithDescription(getStringResource(R.string.voice_search_content_description)),
exists = exists,
)
}
fun searchBookmarkedItem(bookmarkedItem: String) {
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").also {
it.waitForExists(waitingTime)
@ -321,16 +265,9 @@ class BookmarksRobot {
return Transition()
}
fun openThreeDotMenu(bookmarkTitle: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
fun openThreeDotMenu(bookmark: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu")))
threeDotMenu(bookmarkTitle).click()
ThreeDotMenuBookmarksRobot().interact()
return ThreeDotMenuBookmarksRobot.Transition()
}
fun openThreeDotMenu(bookmarkUrl: Uri, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
threeDotMenu(bookmarkUrl).click()
threeDotMenu(bookmark).click()
ThreeDotMenuBookmarksRobot().interact()
return ThreeDotMenuBookmarksRobot.Transition()
@ -357,11 +294,11 @@ class BookmarksRobot {
return BrowserRobot.Transition()
}
fun closeEditBookmarkSection(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
fun closeEditBookmarkSection(interact: BookmarksRobot.() -> Unit): Transition {
goBackButton().click()
BookmarksRobot().interact()
return BookmarksRobot.Transition()
return Transition()
}
fun openBookmarkWithTitle(bookmarkTitle: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
@ -374,6 +311,13 @@ class BookmarksRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickSearchButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
itemWithResId("$packageName:id/bookmark_search").click()
SearchRobot().interact()
return SearchRobot.Transition()
}
}
}
@ -405,17 +349,10 @@ private fun addFolderTitleField() = onView(withId(R.id.bookmarkNameEdit))
private fun saveFolderButton() = onView(withId(R.id.confirm_add_folder_button))
private fun threeDotMenu(bookmarkUrl: Uri) = onView(
allOf(
withId(R.id.overflow_menu),
withParent(withChild(allOf(withId(R.id.url), withText(bookmarkUrl.toString())))),
),
)
private fun threeDotMenu(bookmarkTitle: String) = onView(
private fun threeDotMenu(bookmark: String) = onView(
allOf(
withId(R.id.overflow_menu),
withParent(withChild(allOf(withId(R.id.title), withText(bookmarkTitle)))),
hasSibling(withText(bookmark)),
),
)

@ -99,9 +99,9 @@ class BrowserRobot {
}
/* Asserts that the text within DOM element with ID="testContent" has the given text, i.e.
* document.querySelector('#testContent').innerText == expectedText
*
*/
* document.querySelector('#testContent').innerText == expectedText
*
*/
fun verifyPageContent(expectedText: String) {
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
@ -316,7 +316,18 @@ class BrowserRobot {
}
}
fun longClickPDFImage() = longClickPageObject(itemWithResId("pdfjs_internal_id_8R"))
fun longClickPDFImage() = longClickPageObject(itemWithResId("pdfjs_internal_id_13R"))
fun verifyPDFReaderToolbarItems() {
assertTrue(
itemWithResIdAndText("download", "Download")
.waitForExists(waitingTime),
)
assertTrue(
itemWithResIdAndText("openInApp", "Open in app")
.waitForExists(waitingTime),
)
}
fun clickSubmitLoginButton() {
clickPageObject(itemWithResId("submit"))
@ -869,6 +880,57 @@ class BrowserRobot {
getStringResource(R.string.open_in_app_cfr_negative_button_text),
).click()
fun longClickToolbar() = mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")).click(LONG_CLICK_DURATION)
fun verifyDownloadPromptIsDismissed() =
assertItemWithResIdExists(
itemWithResId("$packageName:id/viewDynamicDownloadDialog"),
exists = false,
)
fun verifyCancelPrivateDownloadsPrompt(numberOfActiveDownloads: String) {
assertItemWithResIdAndTextExists(
itemWithResIdContainingText(
"$packageName:id/title",
getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_warning_content_title),
),
itemWithResIdContainingText(
"$packageName:id/body",
"If you close all Private tabs now, $numberOfActiveDownloads download will be canceled. Are you sure you want to leave Private Browsing?",
),
itemWithResIdContainingText(
"$packageName:id/deny_button",
getStringResource(R.string.mozac_feature_downloads_cancel_active_private_downloads_deny),
),
itemWithResIdContainingText(
"$packageName:id/accept_button",
getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_accept),
),
)
}
fun clickStayInPrivateBrowsingPromptButton() =
itemWithResIdContainingText(
"$packageName:id/deny_button",
getStringResource(R.string.mozac_feature_downloads_cancel_active_private_downloads_deny),
).click()
fun clickCancelPrivateDownloadsPromptButton() {
itemWithResIdContainingText(
"$packageName:id/accept_button",
getStringResource(R.string.mozac_feature_downloads_cancel_active_downloads_accept),
).click()
mDevice.waitForWindowUpdate(packageName, waitingTime)
}
fun fillPdfForm(name: String) {
// Set PDF form text for the text box
itemWithResId("pdfjs_internal_id_10R").setText(name)
// Click PDF form check box
itemWithResId("pdfjs_internal_id_11R").click()
}
class Transition {
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitForIdle(waitingTime)
@ -1110,6 +1172,16 @@ class BrowserRobot {
SettingsRobot().interact()
return SettingsRobot.Transition()
}
fun clickDownloadPDFButton(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
itemWithResIdContainingText(
"download",
"Download",
).click()
DownloadRobot().interact()
return DownloadRobot.Transition()
}
}
}

@ -17,6 +17,9 @@ import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performScrollTo
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeLeft
import androidx.compose.ui.test.swipeRight
import androidx.test.espresso.Espresso
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@ -162,6 +165,20 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
composeTestRule.closeTabButton().performClick()
}
/**
* Swipes a tab with [title] left.
*/
fun swipeTabLeft(title: String) {
composeTestRule.tabItem(title).performTouchInput { swipeLeft() }
}
/**
* Swipes a tab with [title] right.
*/
fun swipeTabRight(title: String) {
composeTestRule.tabItem(title).performTouchInput { swipeRight() }
}
class Transition(private val composeTestRule: HomeActivityComposeTestRule) {
fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
@ -190,10 +207,10 @@ class ComposeTabDrawerRobot(private val composeTestRule: HomeActivityComposeTest
return Transition(composeTestRule)
}
fun closeAllTabs(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
fun closeAllTabs(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
composeTestRule.dropdownMenuItemCloseAllTabs().performClick()
BrowserRobot().interact()
return BrowserRobot.Transition()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {

@ -9,9 +9,12 @@ package org.mozilla.fenix.ui.robots
import android.content.Intent
import android.util.Log
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withId
@ -20,13 +23,18 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.PackageName.GOOGLE_APPS_PHOTOS
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeLong
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.assertExternalAppOpens
import org.mozilla.fenix.helpers.TestHelper.getStringResource
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
@ -73,6 +81,36 @@ class DownloadRobot {
.click()
}
fun deleteDownloadedItem(fileName: String) =
onView(
allOf(
withId(R.id.overflow_menu),
hasSibling(withText(fileName)),
),
).click()
fun longClickDownloadedItem(title: String) =
onView(
allOf(
withId(R.id.title),
withText(title),
),
).perform(longClick())
fun selectDownloadedItem(title: String) =
onView(
allOf(
withId(R.id.title),
withText(title),
),
).perform(click())
fun openMultiSelectMoreOptionsMenu() =
itemWithDescription(getStringResource(R.string.content_description_menu)).click()
fun clickMultiSelectRemoveButton() =
itemWithResIdContainingText("$packageName:id/title", "Remove").click()
class Transition {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {
downloadButton().click()
@ -95,6 +133,13 @@ class DownloadRobot {
return BrowserRobot.Transition()
}
fun closeDownloadPrompt(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
itemWithResId("$packageName:id/download_dialog_close_button").click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickOpen(type: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
openDownloadButton().waitForExists(waitingTime)
openDownloadButton().click()

@ -24,6 +24,10 @@ import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.MatcherHelper.assertItemContainingTextExists
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.getStringResource
@ -118,6 +122,36 @@ class HistoryRobot {
}
}
fun dismissHistorySearchBarUsingBackButton() {
for (i in 1..Constants.RETRY_COUNT) {
try {
mDevice.pressBack()
assertTrue(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime),
)
break
} catch (e: AssertionError) {
if (i == Constants.RETRY_COUNT) {
throw e
}
}
}
}
fun searchForHistoryItem(vararg historyItems: String) {
for (historyItem in historyItems) {
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view").also {
it.waitForExists(waitingTime)
it.setText(historyItem)
}
mDevice.waitForWindowUpdate(packageName, waitingTimeShort)
}
}
fun verifySearchedHistoryItemExists(historyItemUrl: String, exists: Boolean = true) =
assertItemContainingTextExists(itemContainingText(historyItemUrl), exists = exists)
class Transition {
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withContentDescription("Navigate up")).click()
@ -133,6 +167,21 @@ class HistoryRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit): RecentlyClosedTabsRobot.Transition {
recentlyClosedTabsListButton.waitForExists(waitingTime)
recentlyClosedTabsListButton.click()
RecentlyClosedTabsRobot().interact()
return RecentlyClosedTabsRobot.Transition()
}
fun clickSearchButton(interact: SearchRobot.() -> Unit): SearchRobot.Transition {
itemWithResId("$packageName:id/history_search").click()
SearchRobot().interact()
return SearchRobot.Transition()
}
}
}
@ -224,3 +273,6 @@ private fun deleteHistoryEverythingOption() =
.textContains(getStringResource(R.string.delete_history_prompt_button_everything))
.resourceId("$packageName:id/everything_button"),
)
private val recentlyClosedTabsListButton =
mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_tabs_header"))

@ -15,7 +15,6 @@ import androidx.compose.ui.test.assert
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotSelected
import androidx.compose.ui.test.assertIsSelected
import androidx.compose.ui.test.hasTestTag
import androidx.compose.ui.test.hasText
import androidx.compose.ui.test.junit4.ComposeTestRule
import androidx.compose.ui.test.onChildAt
@ -204,7 +203,10 @@ class HomeScreenRobot {
fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertItemWithResIdExists(homepageWordmark)
fun verifyHomeWordmark() {
homeScreenList().scrollToBeginning(3)
assertItemWithResIdExists(homepageWordmark)
}
fun verifyHomeComponent() = assertHomeComponent()
fun verifyDefaultSearchEngine(searchEngine: String) = verifySearchEngineIcon(searchEngine)
fun verifyTabCounter(numberOfOpenTabs: String) =
@ -295,7 +297,10 @@ class HomeScreenRobot {
).waitForExists(waitingTimeShort),
)
fun verifyNotExistingSponsoredTopSitesList() = assertSponsoredTopSitesNotDisplayed()
fun verifyExistingTopSitesTabs(title: String) = assertExistingTopSitesTabs(title)
fun verifyExistingTopSitesTabs(title: String) {
homeScreenList().scrollIntoView(itemWithResId("$packageName:id/top_sites_list"))
assertExistingTopSitesTabs(title)
}
fun verifySponsoredShortcutDetails(sponsoredShortcutTitle: String, position: Int) {
assertSponsoredShortcutLogoIsDisplayed(position)
assertSponsoredShortcutTitle(sponsoredShortcutTitle, position)
@ -387,37 +392,47 @@ class HomeScreenRobot {
}
}
fun scrollToPocketProvokingStories() =
scrollToElementByText(getStringResource(R.string.pocket_stories_categories_header))
fun swipePocketProvokingStories() {
UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList()
.swipeLeft(3)
fun scrollToPocketProvokingStories() {
homeScreenList().scrollIntoView(
mDevice.findObject(UiSelector().resourceId("pocket.recommended.story").index(2)),
)
}
fun verifyPocketRecommendedStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
positions.forEach {
composeTestRule.onNodeWithTag("pocket.stories")
.onChildAt(it - 1)
.assert(hasTestTag("pocket.recommended.story"))
}
}
fun verifyPocketRecommendedStoriesItems() {
for (position in 0..8) {
pocketStoriesList
.scrollIntoView(UiSelector().index(position))
fun verifyPocketSponsoredStoriesItems(composeTestRule: ComposeTestRule, vararg positions: Int) {
composeTestRule.onNodeWithTag("pocket.stories").assertIsDisplayed()
positions.forEach {
composeTestRule.onNodeWithTag("pocket.stories")
.onChildAt(it - 1)
.assert(hasTestTag("pocket.sponsored.story"))
assertTrue(
"Pocket story item at position $position not found.",
mDevice.findObject(UiSelector().index(position))
.waitForExists(waitingTimeShort),
)
}
}
fun verifyDiscoverMoreStoriesButton(composeTestRule: ComposeTestRule, position: Int) {
composeTestRule.onNodeWithTag("pocket.stories")
.assertIsDisplayed()
.onChildAt(position - 1)
.assert(hasTestTag("pocket.discover.more.story"))
// Temporarily not in use because Sponsored Pocket stories are only advertised for a limited time.
// See also known issue https://bugzilla.mozilla.org/show_bug.cgi?id=1828629
// fun verifyPocketSponsoredStoriesItems(vararg positions: Int) {
// positions.forEach {
// pocketStoriesList
// .scrollIntoView(UiSelector().resourceId("pocket.sponsored.story").index(it - 1))
//
// assertTrue(
// "Pocket story item at position $it not found.",
// mDevice.findObject(UiSelector().index(it - 1).resourceId("pocket.sponsored.story"))
// .waitForExists(waitingTimeShort),
// )
// }
// }
fun verifyDiscoverMoreStoriesButton() {
pocketStoriesList
.scrollIntoView(UiSelector().text("Discover more"))
assertTrue(
mDevice.findObject(UiSelector().text("Discover more"))
.waitForExists(waitingTimeShort),
)
}
fun verifyStoriesByTopic(enabled: Boolean) {
@ -444,8 +459,10 @@ class HomeScreenRobot {
}
}
fun verifyStoriesByTopicItems() =
fun verifyStoriesByTopicItems() {
homeScreenList().scrollIntoView(UiSelector().resourceId("pocket.categories"))
assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.categories")).childCount > 1)
}
fun verifyStoriesByTopicItemState(composeTestRule: ComposeTestRule, isSelected: Boolean, position: Int) {
homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
@ -462,10 +479,9 @@ class HomeScreenRobot {
fun clickStoriesByTopicItem(composeTestRule: ComposeTestRule, position: Int) =
storyByTopicItem(composeTestRule, position).performClick()
fun verifyPoweredByPocket(rule: ComposeTestRule) {
fun verifyPoweredByPocket() {
homeScreenList().scrollIntoView(mDevice.findObject(UiSelector().resourceId("pocket.header")))
rule.onNodeWithTag("pocket.header.title", true).assertIsDisplayed()
rule.onNodeWithTag("pocket.header.subtitle", true).assertIsDisplayed()
assertTrue(mDevice.findObject(UiSelector().resourceId("pocket.header.title")).exists())
}
fun verifyCustomizeHomepageButton(enabled: Boolean) {
@ -609,11 +625,18 @@ class HomeScreenRobot {
}
fun togglePrivateBrowsingMode() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
.waitForExists(
waitingTime,
)
privateBrowsingButton.click()
if (
!itemWithResIdAndDescription(
"$packageName:id/privateBrowsingButton",
"Disable private browsing",
).exists()
) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
.waitForExists(
waitingTime,
)
privateBrowsingButton.click()
}
}
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
@ -832,12 +855,14 @@ class HomeScreenRobot {
return BrowserRobot.Transition()
}
fun clickPocketDiscoverMoreButton(composeTestRule: ComposeTestRule, position: Int, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
composeTestRule.onNodeWithTag("pocket.stories")
.assertIsDisplayed()
.onChildAt(position - 1)
.assert(hasTestTag("pocket.discover.more.story"))
.performClick()
fun clickPocketDiscoverMoreButton(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
pocketStoriesList
.scrollIntoView(UiSelector().text("Discover more"))
mDevice.findObject(UiSelector().text("Discover more")).also {
it.waitForExists(waitingTimeShort)
it.click()
}
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -1185,3 +1210,6 @@ private val sponsorsAndPrivacyButton =
.textContains(getStringResource(R.string.top_sites_menu_sponsor_privacy))
.resourceId("$packageName:id/simple_text"),
)
private val pocketStoriesList =
UiScrollable(UiSelector().resourceId("pocket.stories")).setAsHorizontalList()

@ -24,6 +24,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.uiautomator.By
import androidx.test.uiautomator.By.textContains
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
@ -31,6 +32,9 @@ import org.hamcrest.CoreMatchers.not
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdContainingText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
@ -94,12 +98,37 @@ class NavigationToolbarRobot {
}
}
fun longClickEditModeToolbar() =
mDevice.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")).click(LONG_CLICK_DURATION)
fun clickContextMenuItem(item: String) {
mDevice.waitNotNull(
Until.findObject(By.text(item)),
waitingTime,
)
mDevice.findObject(By.text(item)).click()
}
fun clickClearToolbarButton() = clearAddressBarButton().click()
fun verifyToolbarIsEmpty() =
itemWithResIdContainingText(
"$packageName:id/mozac_browser_toolbar_edit_url_view",
getStringResource(R.string.search_hint),
)
fun verifyTextSelectionOptions(vararg textSelectionOptions: String) {
for (textSelectionOption in textSelectionOptions) {
mDevice.waitNotNull(Until.findObject(textContains(textSelectionOption)), waitingTime)
}
}
class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
fun goBackToWebsite(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
openEditURLView()
clearAddressBar().click()
clearAddressBarButton().click()
assertTrue(
mDevice.findObject(
UiSelector()
@ -180,8 +209,8 @@ class NavigationToolbarRobot {
}
fun visitLinkFromClipboard(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
if (clearAddressBar().waitForExists(waitingTimeShort)) {
clearAddressBar().click()
if (clearAddressBarButton().waitForExists(waitingTimeShort)) {
clearAddressBarButton().click()
}
mDevice.waitNotNull(
@ -312,10 +341,7 @@ private fun awesomeBar() =
private fun threeDotButton() = onView(withId(R.id.mozac_browser_toolbar_menu))
private fun tabTrayButton() = onView(withId(R.id.tab_button))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBar() =
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_clear_view"),
)
private fun clearAddressBarButton() = itemWithResId("$packageName:id/mozac_browser_toolbar_clear_view")
private fun goBackButton() = mDevice.pressBack()
private fun readerViewToggle() =
onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))

@ -6,19 +6,17 @@ package org.mozilla.fenix.ui.robots
import android.app.NotificationManager
import android.content.Context
import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.MatcherHelper.itemContainingText
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.appName
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.ext.waitNotNull
import java.lang.AssertionError
import kotlin.AssertionError
class NotificationRobot {
@ -51,16 +49,10 @@ class NotificationRobot {
cancelAll()
}
fun verifySystemNotificationGone(notificationMessage: String) {
mDevice.waitNotNull(
Until.gone(text(notificationMessage)),
waitingTime,
)
fun verifySystemNotificationDoesNotExist(notificationMessage: String) {
mDevice.findObject(UiSelector().textContains(notificationMessage)).waitUntilGone(waitingTime)
assertFalse(
mDevice.findObject(
UiSelector().text(notificationMessage),
).exists(),
mDevice.findObject(UiSelector().textContains(notificationMessage)).waitForExists(waitingTimeShort),
)
}
@ -112,6 +104,60 @@ class NotificationRobot {
}
}
// Performs swipe action on download system notifications
fun swipeDownloadNotification(
direction: String,
shouldDismissNotification: Boolean,
canExpandNotification: Boolean = true,
) {
// In case it fails, retry max 6x the swipe action on download system notifications
for (i in 1..6) {
try {
// Swipe left the download system notification
if (direction == "Left") {
itemContainingText(appName)
.also {
it.waitForExists(waitingTime)
it.swipeLeft(3)
}
} else {
// Swipe right the download system notification
itemContainingText(appName)
.also {
it.waitForExists(waitingTime)
it.swipeRight(3)
}
}
// Not all download related system notifications can be dismissed
if (shouldDismissNotification) {
assertFalse(itemContainingText(appName).waitForExists(waitingTimeShort))
} else {
assertTrue(itemContainingText(appName).waitForExists(waitingTimeShort))
}
break
} catch (e: AssertionError) {
if (i == 6) {
throw e
} else {
notificationShade {
}.closeNotificationTray {
}.openNotificationShade {
// The download complete system notification can't be expanded
if (canExpandNotification) {
// In all cases the download system notification title will be the app name
verifySystemNotificationExists(appName)
expandNotificationMessage()
} else {
// Using the download completed system notification summary to bring in to view an properly verify it
verifySystemNotificationExists("Download completed")
}
}
}
}
}
}
class Transition {
fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
@ -128,6 +174,13 @@ class NotificationRobot {
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun closeNotificationTray(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}

@ -8,6 +8,7 @@ import android.net.Uri
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.withParent
@ -16,7 +17,8 @@ import androidx.test.uiautomator.UiSelector
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.HomeActivityComposeTestRule
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click
@ -29,19 +31,44 @@ class RecentlyClosedTabsRobot {
fun waitForListToExist() =
mDevice.findObject(UiSelector().resourceId("$packageName:id/recently_closed_list"))
.waitForExists(
TestAssetHelper.waitingTime,
)
.waitForExists(waitingTime)
fun verifyRecentlyClosedTabsMenuView() = assertRecentlyClosedTabsMenuView()
fun verifyRecentlyClosedTabsMenuView() {
onView(
allOf(
withText("Recently closed tabs"),
withParent(withId(R.id.navigationToolbar)),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun verifyEmptyRecentlyClosedTabsList() = assertEmptyRecentlyClosedTabsList()
fun verifyEmptyRecentlyClosedTabsList() {
mDevice.waitForIdle()
fun verifyRecentlyClosedTabsPageTitle(title: String) = assertRecentlyClosedTabsPageTitle(title)
onView(
allOf(
withId(R.id.recently_closed_empty_view),
withText(R.string.recently_closed_empty_message),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) = assertPageUrl(expectedUrl)
fun verifyRecentlyClosedTabsPageTitle(title: String) =
recentlyClosedTabsPageTitle(title)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) {
onView(
allOf(
withId(R.id.url),
withEffectiveVisibility(
Visibility.VISIBLE,
),
),
).check(matches(withText(Matchers.containsString(expectedUrl.toString()))))
}
fun clickDeleteRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click()
fun clickDeleteRecentlyClosedTabs() = recentlyClosedTabDeleteButton().click()
class Transition {
fun clickRecentlyClosedItem(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
@ -51,43 +78,36 @@ class RecentlyClosedTabsRobot {
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun assertRecentlyClosedTabsMenuView() {
onView(
allOf(
withText("Recently closed tabs"),
withParent(withId(R.id.navigationToolbar)),
),
)
.check(
matches(withEffectiveVisibility(Visibility.VISIBLE)),
)
}
fun clickOpenInNewTab(testRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInNewTabOption.click()
private fun assertEmptyRecentlyClosedTabsList() {
mDevice.waitForIdle()
ComposeTabDrawerRobot(testRule).interact()
return ComposeTabDrawerRobot.Transition(testRule)
}
onView(
allOf(
withId(R.id.recently_closed_empty_view),
withText(R.string.recently_closed_empty_message),
),
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
fun clickOpenInPrivateTab(testRule: HomeActivityComposeTestRule, interact: ComposeTabDrawerRobot.() -> Unit): ComposeTabDrawerRobot.Transition {
openInPrivateTabOption.click()
private fun assertPageUrl(expectedUrl: Uri) = onView(
allOf(
withId(R.id.url),
withEffectiveVisibility(
Visibility.VISIBLE,
),
),
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString()))),
)
ComposeTabDrawerRobot(testRule).interact()
return ComposeTabDrawerRobot.Transition(testRule)
}
fun clickShare(interact: ShareOverlayRobot.() -> Unit): ShareOverlayRobot.Transition {
multipleSelectionShareButton.click()
ShareOverlayRobot().interact()
return ShareOverlayRobot.Transition()
}
fun goBackToHistoryMenu(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
onView(withContentDescription("Navigate up")).click()
HistoryRobot().interact()
return HistoryRobot.Transition()
}
}
}
private fun recentlyClosedTabsPageTitle(title: String) = onView(
allOf(
@ -96,12 +116,7 @@ private fun recentlyClosedTabsPageTitle(title: String) = onView(
),
)
private fun assertRecentlyClosedTabsPageTitle(title: String) {
recentlyClosedTabsPageTitle(title)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun recentlyClosedTabsDeleteButton() =
private fun recentlyClosedTabDeleteButton() =
onView(
allOf(
withId(R.id.overflow_menu),
@ -110,3 +125,9 @@ private fun recentlyClosedTabsDeleteButton() =
),
),
)
private val openInNewTabOption = onView(withText("Open in new tab"))
private val openInPrivateTabOption = onView(withText("Open in private tab"))
private val multipleSelectionShareButton = onView(withId(R.id.share_history_multi_select))

@ -19,6 +19,7 @@ import androidx.compose.ui.test.performScrollToIndex
import androidx.compose.ui.test.performScrollToNode
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers
@ -38,6 +39,8 @@ import org.mozilla.fenix.helpers.Constants
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
import org.mozilla.fenix.helpers.Constants.SPEECH_RECOGNITION
import org.mozilla.fenix.helpers.MatcherHelper.assertItemWithResIdExists
import org.mozilla.fenix.helpers.MatcherHelper.itemWithDescription
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResId
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndText
import org.mozilla.fenix.helpers.SessionLoadedIdlingResource
@ -55,8 +58,19 @@ import org.mozilla.fenix.helpers.click
* Implementation of Robot Pattern for the search fragment.
*/
class SearchRobot {
fun verifySearchView() = assertSearchView()
fun verifyBrowserToolbar() = assertBrowserToolbarEditView()
fun verifySearchView() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_wrapper"),
).waitForExists(waitingTime),
)
fun verifySearchToolbar(isDisplayed: Boolean) =
assertItemWithResIdExists(
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
exists = isDisplayed,
)
fun verifyScanButton() = assertScanButton()
fun verifyVoiceSearchButtonVisibility(enabled: Boolean) {
@ -127,9 +141,9 @@ class SearchRobot {
fun verifyNoSuggestionsAreDisplayed(rule: ComposeTestRule, vararg searchSuggestions: String) {
rule.waitForIdle()
for (searchSuggestion in searchSuggestions) {
assertFalse(
assertTrue(
mDevice.findObject(UiSelector().textContains(searchSuggestion))
.waitForExists(waitingTimeShort),
.waitUntilGone(waitingTimeShort),
)
}
}
@ -180,9 +194,21 @@ class SearchRobot {
fun verifyKeyboardVisibility() = assertKeyboardVisibility(isExpectedToBeVisible = true)
fun verifySearchEngineList(rule: ComposeTestRule) = rule.assertSearchEngineList()
fun verifySearchEngineIcon(expectedText: String) {
onView(withContentDescription(expectedText))
fun verifySearchSelectorButton() {
assertTrue(itemWithResId("$packageName:id/search_selector").waitForExists(waitingTime))
}
fun verifySearchEngineIcon(name: String) =
assertTrue(itemWithDescription(name).waitForExists(waitingTime))
fun verifySearchBarPlaceholder(text: String) {
assertTrue(
itemWithResIdAndText("$packageName:id/mozac_browser_toolbar_edit_url_view", text)
.waitForExists(waitingTime),
)
}
fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText)
fun verifyEnginesListShortcutContains(rule: ComposeTestRule, searchEngineName: String) = assertEngineListShortcutContains(rule, searchEngineName)
@ -249,6 +275,12 @@ class SearchRobot {
clearButton().click()
}
fun tapOutsideToDismissSearchBar() {
itemWithResId("$packageName:id/search_wrapper").click()
itemWithResId("$packageName:id/mozac_browser_toolbar_edit_url_view")
.waitUntilGone(waitingTime)
}
fun longClickToolbar() {
mDevice.waitForWindowUpdate(packageName, waitingTime)
mDevice.findObject(UiSelector().resourceId("$packageName:id/awesomeBar"))
@ -309,6 +341,17 @@ class SearchRobot {
).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
fun verifySearchBarPosition(bottomPosition: Boolean) {
onView(withId(R.id.toolbar))
.check(
if (bottomPosition) {
PositionAssertions.isCompletelyBelow(withId(R.id.pill_wrapper_divider))
} else {
PositionAssertions.isCompletelyAbove(withId(R.id.pill_wrapper_divider))
},
)
}
class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -404,20 +447,6 @@ private fun assertSearchEnginePrompt(rule: ComposeTestRule, searchEngineName: St
).assertIsDisplayed()
}
private fun assertSearchView() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/search_wrapper"),
).waitForExists(waitingTime),
)
private fun assertBrowserToolbarEditView() =
assertTrue(
mDevice.findObject(
UiSelector().resourceId("$packageName:id/mozac_browser_toolbar_edit_url_view"),
).waitForExists(waitingTime),
)
private fun assertScanButton() =
assertTrue(
scanButton.waitForExists(waitingTime),

@ -217,15 +217,13 @@ class SettingsRobot {
return BrowserRobot.Transition()
}
fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit):
SettingsSubMenuAboutRobot.Transition {
fun openAboutFirefoxPreview(interact: SettingsSubMenuAboutRobot.() -> Unit): SettingsSubMenuAboutRobot.Transition {
aboutFirefoxHeading().click()
SettingsSubMenuAboutRobot().interact()
return SettingsSubMenuAboutRobot.Transition()
}
fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit):
SettingsSubMenuSearchRobot.Transition {
fun openSearchSubMenu(interact: SettingsSubMenuSearchRobot.() -> Unit): SettingsSubMenuSearchRobot.Transition {
itemWithText(getStringResource(R.string.preferences_search))
.also {
it.waitForExists(waitingTimeShort)

@ -173,7 +173,7 @@ fun textSizePercentageEquals(textSizePercentage: Int): ViewAssertion {
val textView = view as TextView
val scaledPixels =
textView.textSize / InstrumentationRegistry.getInstrumentation().context.resources.displayMetrics.scaledDensity
textView.textSize / InstrumentationRegistry.getInstrumentation().context.resources.displayMetrics.density
val currentTextSizePercentage = calculateTextPercentageFromTextSize(scaledPixels)
if (currentTextSizePercentage != textSizePercentage) throw AssertionError("The textview has a text size percentage of $currentTextSizePercentage, and does not match $textSizePercentage")

@ -11,12 +11,14 @@ import android.widget.RelativeLayout
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDescendantOfA
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
@ -49,7 +51,20 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/
class SettingsSubMenuAddonsManagerRobot {
fun verifyAddonPermissionPrompt(addonName: String) = assertAddonPermissionPrompt(addonName)
fun verifyAddonPermissionPrompt(addonName: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add $addonName?")), waitingTime)
onView(
allOf(
withText("Add $addonName?"),
hasSibling(withText(containsString("It requires your permission to:"))),
hasSibling(withText("Add")),
hasSibling(withText("Cancel")),
),
)
.inRoot(isDialog())
.check(matches(isDisplayed()))
}
fun clickInstallAddon(addonName: String) {
mDevice.waitNotNull(
@ -185,25 +200,6 @@ class SettingsSubMenuAddonsManagerRobot {
.check(matches(not(isCompletelyDisplayed())))
}
private fun assertAddonPermissionPrompt(addonName: String) {
onView(allOf(withId(R.id.title), withText("Add $addonName?")))
.check(matches(isCompletelyDisplayed()))
onView(
allOf(
withId(R.id.permissions),
withText(containsString("It requires your permission to:")),
),
)
.check(matches(isCompletelyDisplayed()))
onView(allOf(withId(R.id.allow_button), withText("Add")))
.check(matches(isCompletelyDisplayed()))
onView(allOf(withId(R.id.deny_button), withText("Cancel")))
.check(matches(isCompletelyDisplayed()))
}
private fun assertAddonIsInstalled(addonName: String) {
onView(
allOf(
@ -221,6 +217,8 @@ class SettingsSubMenuAddonsManagerRobot {
}
private fun allowPermissionToInstall() {
mDevice.waitNotNull(Until.findObject(By.text("Add")), waitingTime)
onView(allOf(withId(R.id.allow_button), withText("Add")))
.check(matches(isCompletelyDisplayed()))
.perform(click())

@ -7,7 +7,9 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasAction
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.MatcherHelper.itemWithResIdAndDescription
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.mDevice
@ -22,6 +24,26 @@ class SystemSettingsRobot {
Intents.intended(hasAction(SettingsRobot.DEFAULT_APPS_SETTINGS_ACTION))
}
fun verifyAllSystemNotificationsToggleState(enabled: Boolean) {
if (enabled) {
assertTrue(allSystemSettingsNotificationsToggle.isChecked)
} else {
assertFalse(allSystemSettingsNotificationsToggle.isChecked)
}
}
fun verifyPrivateBrowsingSystemNotificationsToggleState(enabled: Boolean) {
if (enabled) {
assertTrue(privateBrowsingSystemSettingsNotificationsToggle.isChecked)
} else {
assertFalse(privateBrowsingSystemSettingsNotificationsToggle.isChecked)
}
}
fun clickPrivateBrowsingSystemNotificationsToggle() = privateBrowsingSystemSettingsNotificationsToggle.click()
fun clickAllSystemNotificationsToggle() = allSystemSettingsNotificationsToggle.click()
class Transition {
// Difficult to know where this will go
fun goBack(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
@ -46,3 +68,15 @@ private fun assertSystemNotificationsView() {
.waitForExists(waitingTime),
)
}
private val allSystemSettingsNotificationsToggle =
mDevice.findObject(
UiSelector().resourceId("com.android.settings:id/switch_bar")
.childSelector(
UiSelector()
.resourceId("com.android.settings:id/switch_widget")
.index(1),
),
)
private val privateBrowsingSystemSettingsNotificationsToggle =
itemWithResIdAndDescription("com.android.settings:id/switchWidget", "Private browsing session")

@ -6,10 +6,7 @@
package org.mozilla.fenix.ui.robots
import android.content.Context
import android.view.View
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
@ -276,15 +273,7 @@ class TabDrawerRobot {
)
class Transition {
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitForIdle()
Espresso.openActionBarOverflowOrOptionsMenu(ApplicationProvider.getApplicationContext<Context>())
ThreeDotMenuMainRobot().interact()
return ThreeDotMenuMainRobot.Transition()
}
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): Transition {
mDevice.waitForIdle(waitingTime)
tabsCounter().click()
mDevice.waitNotNull(
@ -293,7 +282,7 @@ class TabDrawerRobot {
)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
return Transition()
}
fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
@ -419,8 +408,7 @@ class TabDrawerRobot {
return Transition()
}
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit):
RecentlyClosedTabsRobot.Transition {
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit): RecentlyClosedTabsRobot.Transition {
threeDotMenu().click()
mDevice.waitNotNull(
@ -435,8 +423,7 @@ class TabDrawerRobot {
return RecentlyClosedTabsRobot.Transition()
}
fun clickSaveCollection(interact: CollectionRobot.() -> Unit):
CollectionRobot.Transition {
fun clickSaveCollection(interact: CollectionRobot.() -> Unit): CollectionRobot.Transition {
saveTabsToCollectionButton().click()
CollectionRobot().interact()

@ -27,6 +27,7 @@ import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.Constants.LONG_CLICK_DURATION
import org.mozilla.fenix.helpers.Constants.RETRY_COUNT
@ -101,10 +102,17 @@ class ThreeDotMenuMainRobot {
addToHomeScreenButton,
addToShortcutsButton,
saveToCollectionButton,
settingsButton(),
)
assertCheckedItemWithResIdAndTextExists(addBookmarkButton)
assertCheckedItemWithResIdAndTextExists(desktopSiteToggle(isRequestDesktopSiteEnabled))
// Swipe to second part of menu
expandMenu()
assertItemContainingTextExists(
settingsButton(),
)
if (FeatureFlags.print) {
assertItemContainingTextExists(printContentButton)
}
assertItemWithDescriptionExists(
backButton,
forwardButton,
@ -582,6 +590,7 @@ private val reportSiteIssueButton = itemContainingText("Report Site Issue")
private val addToHomeScreenButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_homescreen))
private val addToShortcutsButton = itemContainingText(getStringResource(R.string.browser_menu_add_to_shortcuts))
private val saveToCollectionButton = itemContainingText(getStringResource(R.string.browser_menu_save_to_collection_2))
private val printContentButton = itemContainingText(getStringResource(R.string.menu_print))
private val backButton = itemWithDescription(getStringResource(R.string.browser_menu_back))
private val forwardButton = itemWithDescription(getStringResource(R.string.browser_menu_forward))
private val shareButton = itemWithDescription(getStringResource(R.string.share_button_content_description))

@ -0,0 +1,563 @@
<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt" >
<aapt:attr name="android:drawable">
<vector
android:width="432dp"
android:height="432dp"
android:viewportWidth="432"
android:viewportHeight="432">
<group
android:name="a1_t"
android:pivotX="216"
android:pivotY="216">
<path
android:pathData="M303.8,251.4h-79.2c-2.3,0 -4.2,1.9 -4.2,4.2v12.7c0,18.7 15.1,33.8 33.8,33.8l0,0H298c14,0 25.3,-11.4 25.3,-25.3v-11.1C323.3,259.3 317,251.4 303.8,251.4L303.8,251.4z"
android:fillColor="#008787"/>
<path
android:pathData="M303.8,251.4h-79.2c-2.3,0 -4.2,1.9 -4.2,4.2v12.7c0,18.7 15.1,33.8 33.8,33.8l0,0H298c14,0 25.3,-11.4 25.3,-25.3v-11.1C323.3,259.3 317,251.4 303.8,251.4L303.8,251.4z"
android:strokeAlpha="0.9"
android:fillAlpha="0.9">
<aapt:attr name="android:fillColor">
<gradient
android:startX="301.7"
android:startY="300.03"
android:endX="274.29"
android:endY="274.08"
android:type="linear">
<item android:offset="0" android:color="#7F054096"/>
<item android:offset="0.05" android:color="#700F3D9C"/>
<item android:offset="0.26" android:color="#3F2F35B1"/>
<item android:offset="0.47" android:color="#1C462FBF"/>
<item android:offset="0.67" android:color="#07542BC8"/>
<item android:offset="0.86" android:color="#00592ACB"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.2c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8c0,0.2 -0.1,0.5 -0.1,0.7c-0.2,0.9 -0.9,5.7 -1.1,6.8c0,0.1 0,0.1 0,0.2c-0.9,4.9 -1.5,9.9 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.7 48.5,108.2 108.3,108.2c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2zM191.5,263.9c0.5,0.2 1,0.5 1.5,0.7c0,0 0,0 0.1,0C192.6,264.5 192.1,264.2 191.5,263.9zM305.7,184.8L305.7,184.8L305.7,184.8L305.7,184.8z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="296.59"
android:startY="138.83"
android:endX="128.16"
android:endY="312.38"
android:type="linear">
<item android:offset="0.05" android:color="#FFFFF44F"/>
<item android:offset="0.37" android:color="#FFFF980E"/>
<item android:offset="0.53" android:color="#FFFF3647"/>
<item android:offset="0.7" android:color="#FFE31587"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.1v0.1c10.2,27.7 4.6,55.9 -3.4,73.1c-12.4,26.6 -42.5,54 -89.5,52.6c-50.8,-1.4 -95.5,-39.2 -103.9,-88.6c-1.5,-7.8 0,-11.8 0.8,-18.1c-0.9,4.9 -1.3,6.3 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.8 48.5,108.3 108.3,108.3c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="297.63"
android:centerY="151.73"
android:gradientRadius="221.61"
android:type="radial">
<item android:offset="0.13" android:color="#FFFFBD4F"/>
<item android:offset="0.28" android:color="#FFFF980E"/>
<item android:offset="0.47" android:color="#FFFF3750"/>
<item android:offset="0.78" android:color="#FFEB0878"/>
<item android:offset="0.86" android:color="#FFE50080"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.1v0.1c10.2,27.7 4.6,55.9 -3.4,73.1c-12.4,26.6 -42.5,54 -89.5,52.6c-50.8,-1.4 -95.5,-39.2 -103.9,-88.6c-1.5,-7.8 0,-11.8 0.8,-18.1c-0.9,4.9 -1.3,6.3 -1.8,15c0,0.2 0,0.4 0,0.5c0,59.8 48.5,108.3 108.3,108.3c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="213.81"
android:centerY="222.16"
android:gradientRadius="227.15"
android:type="radial">
<item android:offset="0.3" android:color="#FF960E18"/>
<item android:offset="0.35" android:color="#BCB11927"/>
<item android:offset="0.43" android:color="#56DB293D"/>
<item android:offset="0.5" android:color="#16F5334B"/>
<item android:offset="0.53" android:color="#00FF3750"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M263.7,191.9c0.2,0.2 0.5,0.3 0.7,0.5c-2.7,-4.8 -6.1,-9.2 -10.1,-13.1c-33.7,-33.7 -8.8,-73 -4.6,-75l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c1.3,-0.1 2.5,-0.2 3.8,-0.2C236.6,164.2 254.3,175.4 263.7,191.9L263.7,191.9z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="237.41"
android:centerY="93.62"
android:gradientRadius="72.74"
android:type="radial">
<item android:offset="0.13" android:color="#FFFFF44F"/>
<item android:offset="0.53" android:color="#FFFF980E"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M216.4,198.6c-0.2,2.7 -9.7,12 -13,12c-30.8,0 -35.8,18.6 -35.8,18.6c1.4,15.7 12.3,28.6 25.5,35.4c0.6,0.3 1.2,0.6 1.8,0.9c1,0.5 2.1,0.9 3.2,1.3c4.6,1.6 9.3,2.5 14.1,2.7c54,2.5 64.5,-64.6 25.5,-84c10,-1.8 20.3,2.3 26.1,6.4c-9.5,-16.5 -27.1,-27.7 -47.5,-27.7c-1.3,0 -2.5,0.1 -3.8,0.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c1.7,1.4 3.5,3.2 7.4,7.1C197.5,190.2 216.3,197.8 216.4,198.6L216.4,198.6z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="189.41"
android:centerY="280.33"
android:gradientRadius="96.24"
android:type="radial">
<item android:offset="0.35" android:color="#FF3A8EE6"/>
<item android:offset="0.67" android:color="#FF9059FF"/>
<item android:offset="1" android:color="#FFC139E6"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M216.4,198.6c-0.2,2.7 -9.7,12 -13,12c-30.8,0 -35.8,18.6 -35.8,18.6c1.4,15.7 12.3,28.6 25.5,35.4c0.6,0.3 1.2,0.6 1.8,0.9c1,0.5 2.1,0.9 3.2,1.3c4.6,1.6 9.3,2.5 14.1,2.7c54,2.5 64.5,-64.6 25.5,-84c10,-1.8 20.3,2.3 26.1,6.4c-9.5,-16.5 -27.1,-27.7 -47.5,-27.7c-1.3,0 -2.5,0.1 -3.8,0.2c-10.9,0.8 -21.3,4.7 -29.8,11.5c1.7,1.4 3.5,3.2 7.4,7.1C197.5,190.2 216.3,197.8 216.4,198.6L216.4,198.6z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="212.55"
android:centerY="199"
android:gradientRadius="51.11"
android:type="radial">
<item android:offset="0.21" android:color="#009059FF"/>
<item android:offset="0.97" android:color="#996E008B"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M177.6,172.2c0.8,0.5 1.5,1 2.3,1.5c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2C154,167.4 169.7,167.1 177.6,172.2L177.6,172.2z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="208.87"
android:centerY="120.79"
android:gradientRadius="76.52"
android:type="radial">
<item android:offset="0.1" android:color="#FFFFE226"/>
<item android:offset="0.79" android:color="#FFFF7139"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M108.8,222c8.4,49.4 53.2,87.1 104,88.5c47.1,1.4 77.1,-26 89.5,-52.6c8,-17.2 13.6,-45.4 3.4,-73.1l0,0v-0.1c0,-0.1 0,-0.1 0,-0.1s0,0 0,0.2c3.8,25.1 -8.9,49.4 -28.9,65.8v0.1c-38.9,31.6 -76.1,19.1 -83.6,14c-0.5,-0.3 -1,-0.5 -1.6,-0.8c-22.7,-10.8 -32.1,-31.5 -30,-49.2c-19.2,0 -25.7,-16.1 -25.7,-16.1s17.2,-12.3 39.9,-1.6c21,9.9 40.7,1.6 40.7,1.6c0,-0.9 -18.9,-8.4 -26.2,-15.6c-3.9,-3.9 -5.8,-5.7 -7.4,-7.1c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-0.7,-0.5 -1.5,-1 -2.3,-1.5c-7.9,-5.1 -23.6,-4.9 -24.1,-4.8l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8C110.7,196.5 106.9,213.3 108.8,222L108.8,222z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="278.74"
android:centerY="70.41"
android:gradientRadius="365.3"
android:type="radial">
<item android:offset="0.11" android:color="#FFFFF44F"/>
<item android:offset="0.46" android:color="#FFFF980E"/>
<item android:offset="0.72" android:color="#FFFF3647"/>
<item android:offset="0.9" android:color="#FFE31587"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M254.4,179.3c4,3.9 7.3,8.3 10.1,13.1c0.6,0.5 1.2,0.9 1.6,1.4c24.6,22.6 11.7,54.6 10.7,56.9c20,-16.5 32.7,-40.8 28.9,-65.8c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0C245.5,106.2 220.7,145.6 254.4,179.3L254.4,179.3z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="247.82"
android:centerY="76.43"
android:gradientRadius="240.17"
android:type="radial">
<item android:offset="0" android:color="#FFFFF44F"/>
<item android:offset="0.3" android:color="#FFFF980E"/>
<item android:offset="0.57" android:color="#FFFF3647"/>
<item android:offset="0.74" android:color="#FFE31587"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M266,193.8c-0.5,-0.5 -1,-0.9 -1.6,-1.4c-0.2,-0.2 -0.5,-0.3 -0.7,-0.5c-5.8,-4.1 -16.1,-8.1 -26.1,-6.4c39,19.5 28.5,86.6 -25.5,84c-4.8,-0.2 -9.6,-1.1 -14.1,-2.7c-1.1,-0.4 -2.1,-0.9 -3.2,-1.3c-0.6,-0.3 -1.2,-0.5 -1.8,-0.9c0,0 0,0 0.1,0c7.5,5.1 44.7,17.7 83.6,-14v-0.1C277.8,248.4 290.6,216.4 266,193.8L266,193.8z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="209.65"
android:centerY="148.39"
android:gradientRadius="209.74"
android:type="radial">
<item android:offset="0.14" android:color="#FFFFF44F"/>
<item android:offset="0.48" android:color="#FFFF980E"/>
<item android:offset="0.66" android:color="#FFFF3647"/>
<item android:offset="0.9" android:color="#FFE31587"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M167.5,229.2c0,0 5,-18.6 35.8,-18.6c3.3,0 12.8,-9.3 13,-12s-19.7,8.3 -40.7,-1.6c-22.6,-10.6 -39.9,1.6 -39.9,1.6s6.5,16.1 25.7,16.1c-2,17.7 7.3,38.4 30,49.2c0.5,0.2 1,0.5 1.5,0.7C179.8,257.9 168.9,245 167.5,229.2z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="264.09"
android:centerY="160.45"
android:gradientRadius="252.1"
android:type="radial">
<item android:offset="0.09" android:color="#FFFFF44F"/>
<item android:offset="0.63" android:color="#FFFF980E"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M316.4,179.2c-4.7,-11.3 -14.2,-23.5 -21.7,-27.4c6.1,11.9 9.6,23.9 11,32.9c0,0 0,0 0,0.2c-12.3,-30.5 -33,-42.8 -50,-69.7c-0.9,-1.4 -1.7,-2.7 -2.6,-4.1c-0.4,-0.7 -0.8,-1.5 -1.2,-2.3c-0.7,-1.4 -1.3,-2.8 -1.6,-4.3c0,-0.1 -0.1,-0.3 -0.2,-0.3c0,0 -0.1,0 -0.2,0l0,0c0,0 0,0 -0.1,0l0,0c-27.2,16 -36.5,45.4 -37.3,60.2c1.3,-0.1 2.5,-0.2 3.8,-0.2c20.3,0 38,11.2 47.5,27.7c-5.8,-4.1 -16.1,-8.1 -26.1,-6.4c39,19.5 28.5,86.6 -25.5,84c-4.8,-0.2 -9.6,-1.1 -14.1,-2.7c-1.1,-0.4 -2.1,-0.9 -3.2,-1.3c-0.6,-0.3 -1.2,-0.5 -1.8,-0.9c0,0 0,0 0.1,0c-0.5,-0.3 -1,-0.5 -1.6,-0.8c0.5,0.2 1,0.5 1.5,0.7c-13.3,-6.9 -24.2,-19.7 -25.5,-35.4c0,0 5,-18.6 35.8,-18.6c3.3,0 12.8,-9.3 13,-12c0,-0.9 -18.9,-8.4 -26.2,-15.6c-3.9,-3.9 -5.8,-5.7 -7.4,-7.1c-0.9,-0.8 -1.8,-1.4 -2.8,-2.1c-2.5,-8.7 -2.6,-17.8 -0.3,-26.5c-11.1,5 -19.8,13.1 -26.1,20.2l0,0c-4.3,-5.5 -4,-23.4 -3.7,-27.1c0,-0.2 -3.2,1.6 -3.6,1.9c-3.8,2.7 -7.3,5.7 -10.6,9.1c-3.7,3.7 -7.1,7.8 -10.1,12.1l0,0l0,0c-7,9.9 -11.9,21 -14.5,32.8c0,0.2 -0.1,0.5 -0.1,0.7c-0.2,0.9 -1.1,5.8 -1.3,6.9c0,0.1 0,-0.1 0,0c-0.9,5 -1.4,10.1 -1.6,15.1c0,0.2 0,0.4 0,0.5c0,59.7 48.5,108.2 108.3,108.2c53.6,0 98.1,-38.9 106.8,-90c0.2,-1.4 0.3,-2.8 0.5,-4.2C325.6,215 323.2,195.5 316.4,179.2zM305.7,184.7v0.1l0,0V184.7z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="285.98"
android:startY="140.07"
android:endX="151.82"
android:endY="296.61"
android:type="linear">
<item android:offset="0.17" android:color="#CCFFF44F"/>
<item android:offset="0.6" android:color="#00FFF44F"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M319.1,269.9h-72.8c-17.2,0 -31.2,14 -31.2,31.2l0,0v15.2c0,2.3 1.9,4.2 4.2,4.2h72.8c17.2,0 31.2,-14 31.2,-31.2v-23.7C323.3,268 321.4,269.9 319.1,269.9L319.1,269.9z">
<aapt:attr name="android:fillColor">
<gradient
android:startX="227.01"
android:startY="274.49"
android:endX="325.95"
android:endY="316.35"
android:type="linear">
<item android:offset="0" android:color="#FF54FFBD"/>
<item android:offset="1" android:color="#FF00DDFF"/>
</gradient>
</aapt:attr>
</path>
<path
android:pathData="M247.6,295c1.5,-0.9 2.5,-2.5 2.4,-4.2c0,-3.7 -2.6,-5.9 -7.3,-5.9H234v21h8.7c4.6,0 7.5,-2.1 7.5,-6.2C250.4,297.4 249.4,295.8 247.6,295L247.6,295zM238.1,288.4h4.8c2.1,0 3.1,0.9 3.1,2.3s-0.9,2.5 -3.1,2.5h-4.8V288.4zM242.9,302.2h-4.8V297h4.6c2.6,0 3.5,0.9 3.5,2.6C246.3,301.1 245.1,302.2 242.9,302.2L242.9,302.2zM254.3,305.8h14.1V302h-10.1v-4.8h10.1v-3.8h-10.1v-4.7h10.1v-3.8h-14.1L254.3,305.8L254.3,305.8zM287.4,284.9h-15.6v3.7h5.8v17.2h4v-17.3h5.8L287.4,284.9zM298.3,284.9h-4l-7.9,21h4.1l1.4,-3.8h8.7l1.4,3.8h4.1L298.3,284.9zM293.3,298.3l3.1,-8.3l3,8.3H293.3z"
android:fillColor="#20123A"/>
</group>
</vector>
</aapt:attr>
<target
android:name="a1_t">
<aapt:attr
name="android:animation">
<set>
<objectAnimator
android:propertyName="scaleX"
android:duration="300"
android:valueFrom="0.75"
android:valueTo="0.75"
android:valueType="floatType"
android:interpolator="@android:interpolator/linear"/>
<objectAnimator
android:propertyName="scaleX"
android:duration="700"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="300"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="1000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="2000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="3000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="4000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="5000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="6000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="7000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="8000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="9000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="10000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleX"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="11000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="300"
android:valueFrom="0.75002400000000002"
android:valueTo="0.75002400000000002"
android:valueType="floatType"
android:interpolator="@android:interpolator/linear"/>
<objectAnimator
android:propertyName="scaleY"
android:duration="700"
android:valueFrom="0.75002400000000002"
android:valueTo="1"
android:startOffset="300"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="1000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="2000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="3000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="4000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="5000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="6000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="7000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="8000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="9000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="0.75"
android:valueTo="1"
android:startOffset="10000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
<objectAnimator
android:propertyName="scaleY"
android:duration="1000"
android:valueFrom="1"
android:valueTo="0.75"
android:startOffset="11000"
android:valueType="floatType">
<aapt:attr
name="android:interpolator">
<pathInterpolator
android:pathData="M0,0 C0.5,0 0.5,1 1,1"/>
</aapt:attr>
</objectAnimator>
</set>
</aapt:attr>
</target>
</animated-vector>

File diff suppressed because one or more lines are too long

@ -99,6 +99,7 @@
<activity
android:name=".HomeActivity"
android:theme="@style/SplashScreen"
android:exported="true"
android:configChanges="keyboard|keyboardHidden|mcc|mnc|orientation|screenSize|layoutDirection|smallestScreenSize|screenLayout"
android:launchMode="singleTask"
@ -145,6 +146,8 @@
android:host="urls_bookmarks"/>
<data android:scheme="${deepLinkScheme}"
android:host="urls_history"/>
<data android:scheme="${deepLinkScheme}"
android:host="test_deferred_deep_link"/>
</intent-filter>
</activity>

@ -47,11 +47,6 @@ object FeatureFlags {
return isPocketRecommendationsFeatureEnabled(context)
}
/**
* Enables the Unified Search feature.
*/
const val unifiedSearchFeature = true
/**
* Enables compose on the tabs tray items.
*/
@ -62,19 +57,14 @@ object FeatureFlags {
*/
const val composeTopSites = false
/**
* Enables the save to PDF feature.
*/
const val saveToPDF = true
/**
* Enables the notification pre permission prompt.
*/
const val notificationPrePermissionPromptEnabled = true
/**
* Enables new search settings UI with two extra fragments, for managing the default engine
* and managing search shortcuts in the quick search menu.
*/
const val unifiedSearchSettings = true
/**
* Enables printing from the share and primary menu.
*/
val print = Config.channel.isNightlyOrDebug
}

@ -5,6 +5,8 @@
package org.mozilla.fenix
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
@ -21,6 +23,7 @@ import androidx.work.Configuration.Provider
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
@ -48,6 +51,8 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import mozilla.components.support.base.ext.isNotificationChannelEnabled
import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger
@ -59,13 +64,17 @@ import mozilla.components.support.locale.LocaleAwareApplication
import mozilla.components.support.rusterrors.initializeRustErrors
import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.utils.BrowsersCache
import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.Addresses
import org.mozilla.fenix.GleanMetrics.AndroidAutofill
import org.mozilla.fenix.GleanMetrics.CreditCards
import org.mozilla.fenix.GleanMetrics.CustomizeHome
import org.mozilla.fenix.GleanMetrics.Events.marketingNotificationAllowed
import org.mozilla.fenix.GleanMetrics.GleanBuildInfo
import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.GleanMetrics.Preferences
@ -77,12 +86,11 @@ import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.MozillaProductDetector
import org.mozilla.fenix.experiments.maybeFetchExperiments
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.containsQueryParameters
import org.mozilla.fenix.ext.getCustomGleanServerUrlIfAvailable
import org.mozilla.fenix.ext.isCustomEngine
import org.mozilla.fenix.ext.isKnownSearchDomain
import org.mozilla.fenix.ext.isNotificationChannelEnabled
import org.mozilla.fenix.ext.setCustomEndpointIfAvailable
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.lifecycle.StoreLifecycleObserver
@ -97,13 +105,25 @@ import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.utils.Settings.Companion.TOP_SITES_PROVIDER_MAX_THRESHOLD
import org.mozilla.fenix.wallpapers.Wallpaper
import java.util.UUID
import java.util.concurrent.TimeUnit
/**
* The actual RAM threshold is 2GB.
*
* To enable simpler reporting, we want to use the device's 'advertised' RAM.
* As [ActivityManager.MemoryInfo.totalMem] is not the device's 'advertised' RAM spec & we cannot
* access [ActivityManager.MemoryInfo.advertisedMem] across all Android versions, we will use a
* proxy value of 1.6GB. This is based on 1.5GB with a small 'excess' buffer. We assert that all
* values above this proxy value are 2GB or more.
*/
private const val RAM_THRESHOLD_PROXY_GB = 1.6F
private const val RAM_THRESHOLD_BYTES = RAM_THRESHOLD_PROXY_GB * (1e+9).toLong()
/**
*The main application class for Fenix. Records data to measure initialization performance.
* Installs [CrashReporter], initializes [Glean] in fenix builds and setup Megazord in the main process.
@ -422,6 +442,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
private fun startMetricsIfEnabled() {
if (settings().isTelemetryEnabled) {
components.analytics.metrics.start(MetricServiceType.Data)
components.analytics.crashFactCollector.start()
}
if (settings().isMarketingTelemetryEnabled) {
@ -698,6 +719,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
settings: Settings,
browsersCache: BrowsersCache = BrowsersCache,
mozillaProductDetector: MozillaProductDetector = MozillaProductDetector,
isDeviceRamAboveThreshold: Boolean = isDeviceRamAboveThreshold(),
) {
setPreferenceMetrics(settings)
with(Metrics) {
@ -733,18 +755,17 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
searchWidgetInstalled.set(settings.searchWidgetInstalled)
if (settings.sharedPrefsUUID.isEmpty()) {
settings.sharedPrefsUUID = sharedPrefsUuid.generateAndSet().toString()
} else {
sharedPrefsUuid.set(UUID.fromString(settings.sharedPrefsUUID))
}
val openTabsCount = settings.openTabsCount
hasOpenTabs.set(openTabsCount > 0)
if (openTabsCount > 0) {
tabsOpenCount.add(openTabsCount)
}
val openPrivateTabsCount = settings.openPrivateTabsCount
if (openPrivateTabsCount > 0) {
privateTabsOpenCount.add(openPrivateTabsCount)
}
val topSitesSize = settings.topSitesSize
hasTopSites.set(topSitesSize > 0)
if (topSitesSize > 0) {
@ -796,6 +817,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
marketingNotificationAllowed.set(
notificationManagerCompat.isNotificationChannelEnabled(MARKETING_CHANNEL_ID),
)
ramMoreThanThreshold.set(isDeviceRamAboveThreshold)
}
with(AndroidAutofill) {
@ -824,8 +847,28 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
migrateTopicSpecificSearchEngines()
}
}
@OptIn(DelicateCoroutinesApi::class)
GlobalScope.launch(IO) {
val autoFillStorage = applicationContext.components.core.autofillStorage
Addresses.savedAll.set(autoFillStorage.getAllAddresses().size.toLong())
CreditCards.savedAll.set(autoFillStorage.getAllCreditCards().size.toLong())
val lazyPasswordStorage = applicationContext.components.core.lazyPasswordsStorage
Logins.savedAll.set(lazyPasswordStorage.value.list().size.toLong())
}
}
private fun deviceRamBytes(): Long {
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
return memoryInfo.totalMem
}
private fun isDeviceRamAboveThreshold() = deviceRamBytes() > RAM_THRESHOLD_BYTES
@Suppress("ComplexMethod")
private fun setPreferenceMetrics(
settings: Settings,

@ -33,6 +33,7 @@ import androidx.annotation.VisibleForTesting.Companion.PROTECTED
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
@ -43,6 +44,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
@ -55,14 +57,13 @@ import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.WebExtensionState
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.concept.storage.BookmarkNodeType
import mozilla.components.concept.storage.HistoryMetadataKey
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.media.ext.findActiveMediaTab
import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.ext.areNotificationsEnabledSafe
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.log.logger.Logger
@ -74,6 +75,7 @@ import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.support.ktx.kotlin.toNormalizedUrl
import mozilla.components.support.locale.LocaleAwareAppCompatActivity
import mozilla.components.support.utils.BootUtils
import mozilla.components.support.utils.BrowsersCache
import mozilla.components.support.utils.ManufacturerCodes
import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent
@ -82,6 +84,8 @@ import mozilla.telemetry.glean.private.NoExtras
import org.mozilla.experiments.nimbus.initializeTooling
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution
import org.mozilla.fenix.GleanMetrics.SplashScreen
import org.mozilla.fenix.GleanMetrics.StartOnHome
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
@ -95,7 +99,6 @@ import org.mozilla.fenix.databinding.ActivityHomeBinding
import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections
import org.mozilla.fenix.experiments.ResearchSurfaceDialogFragment
import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.areNotificationsEnabledSafe
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hasTopDestination
@ -153,7 +156,6 @@ import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.trackingprotection.TrackingProtectionPanelDialogFragmentDirections
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference
import java.util.Locale
@ -231,6 +233,9 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
MarkersFragmentLifecycleCallbacks.register(supportFragmentManager, components.core.engine)
PlayStoreAttribution.deferredDeeplinkTime.start()
maybeShowSplashScreen()
// 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
@ -413,6 +418,39 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
}
private fun maybeShowSplashScreen() {
if (components.settings.isFirstSplashScreenShown) {
return
} else {
components.settings.isFirstSplashScreenShown = true
// Splash screen compat fails to draw icons on earlier versions.
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
return
}
}
if (FxNimbus.features.splashScreen.value().enabled) {
val splashScreen = installSplashScreen()
var maxDurationReached = false
val delay = FxNimbus.features.splashScreen.value().maximumDurationMs.toLong()
splashScreen.setKeepOnScreenCondition {
val dataFetched = components.settings.utmParamsKnown &&
components.settings.nimbusExperimentsFetched
val keepOnScreen = !maxDurationReached && !dataFetched
if (!keepOnScreen) {
SplashScreen.firstLaunchExtended.record(
SplashScreen.FirstLaunchExtendedExtra(dataFetched = dataFetched),
)
}
keepOnScreen
}
MainScope().launch {
delay(timeMillis = delay)
maxDurationReached = true
}
}
}
private fun checkAndExitPiP() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
// Exit PiP mode
@ -502,6 +540,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
"finishing" to isFinishing.toString(),
),
)
PlayStoreAttribution.deferredDeeplinkTime.cancel()
}
final override fun onPause() {
@ -515,11 +555,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
applicationContext,
showMobileRoot = false,
).withOptionalDesktopFolders(it)
settings().desktopBookmarksSize = getBookmarkCount(desktopRootNode)
settings().desktopBookmarksSize = desktopRootNode.count()
}
components.core.bookmarksStorage.getTree(BookmarkRoot.Mobile.id, true)?.let {
settings().mobileBookmarksSize = getBookmarkCount(it)
settings().mobileBookmarksSize = it.count()
}
}
@ -549,25 +589,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
outContent?.webUri = currentTabUrl?.let { Uri.parse(it) }
}
private fun getBookmarkCount(node: BookmarkNode): Int {
val children = node.children
return if (children == null) {
0
} else {
var count = 0
for (child in children) {
if (child.type == BookmarkNodeType.FOLDER) {
count += getBookmarkCount(child)
} else if (child.type == BookmarkNodeType.ITEM) {
count++
}
}
count
}
}
override fun onDestroy() {
super.onDestroy()

@ -46,6 +46,7 @@ abstract class AddonPopupBaseFragment : Fragment(), EngineSession.Observer, User
onNeedToRequestPermissions = { permissions ->
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
},
tabsUseCases = requireComponents.useCases.tabsUseCases,
),
owner = this,
view = view,

@ -9,13 +9,7 @@ import android.graphics.Typeface
import android.graphics.fonts.FontStyle.FONT_WEIGHT_MEDIUM
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.accessibility.AccessibilityEvent
import android.view.inputmethod.EditorInfo
import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.SearchView
import androidx.core.view.MenuHost
@ -32,23 +26,24 @@ import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.launch
import mozilla.components.concept.engine.webextension.WebExtensionInstallException
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.AddonsManagerAdapter
import mozilla.components.feature.addons.ui.translateName
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.databinding.FragmentAddOnsManagementBinding
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.extension.WebExtensionPromptFeature
import org.mozilla.fenix.theme.ThemeManager
import java.lang.ref.WeakReference
import java.util.Locale
import java.util.concurrent.CancellationException
/**
@ -63,6 +58,9 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private var binding: FragmentAddOnsManagementBinding? = null
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private var addons: List<Addon> = emptyList()
/**
* Whether or not an add-on installation is in progress.
*/
@ -88,6 +86,22 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
binding = FragmentAddOnsManagementBinding.bind(view)
bindRecyclerView()
setupMenu()
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = requireComponents.core.store,
provideAddons = { addons },
context = requireContext(),
fragmentManager = parentFragmentManager,
view = view,
onAddonChanged = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
}
},
),
owner = this,
view = view,
)
}
@ -176,15 +190,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
view?.hideKeyboard()
}
override fun onStart() {
logger.info("Started AddonsManagementFragment")
super.onStart()
findPreviousDialogFragment()?.let { dialog ->
dialog.onPositiveButtonClicked = onPositiveButtonClicked
}
}
override fun onDestroyView() {
logger.info("Destroyed view for AddonsManagementFragment")
@ -199,7 +204,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
val managementView = AddonsManagementView(
navController = findNavController(),
showPermissionDialog = ::showPermissionDialog,
onInstallButtonClicked = ::installAddon,
)
val recyclerView = binding?.addOnsList
@ -266,7 +271,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
if (addonToInstall.isInstalled()) {
showErrorSnackBar(getString(R.string.addon_already_installed))
} else {
showPermissionDialog(addonToInstall)
installAddon(addonToInstall)
}
}
installExternalAddonComplete = true
@ -297,113 +302,19 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
)
}
private fun findPreviousDialogFragment(): PermissionsDialogFragment? {
return parentFragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment
}
private fun hasExistingPermissionDialogFragment(): Boolean {
return findPreviousDialogFragment() != null
}
private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? PagedAddonInstallationDialogFragment != null
}
@VisibleForTesting
internal fun showPermissionDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) {
val dialog = PermissionsDialogFragment.newInstance(
addon = addon,
promptsStyling = PermissionsDialogFragment.PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
positiveButtonBackgroundColor = ThemeManager.resolveAttribute(
R.attr.accent,
requireContext(),
),
positiveButtonTextColor = ThemeManager.resolveAttribute(
R.attr.textOnColorPrimary,
requireContext(),
),
positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(),
),
onPositiveButtonClicked = onPositiveButtonClicked,
)
dialog.show(parentFragmentManager, PERMISSIONS_DIALOG_FRAGMENT_TAG)
}
}
private fun showInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonInstallationDialogFragment()) {
val context = requireContext()
val addonCollectionProvider = context.components.addonCollectionProvider
// Fragment may not be attached to the context anymore during onConfirmButtonClicked handling,
// but we still want to be able to process user selection of the 'allowInPrivateBrowsing' pref.
// This is a best-effort attempt to do so - retain a weak reference to the application context
// (to avoid a leak), which we attempt to use to access addonManager.
// See https://github.com/mozilla-mobile/fenix/issues/15816
val weakApplicationContext: WeakReference<Context> = WeakReference(context)
val dialog = PagedAddonInstallationDialogFragment.newInstance(
addon = addon,
addonCollectionProvider = addonCollectionProvider,
promptsStyling = PagedAddonInstallationDialogFragment.PromptsStyling(
gravity = Gravity.BOTTOM,
shouldWidthMatchParent = true,
confirmButtonBackgroundColor = ThemeManager.resolveAttribute(
R.attr.accent,
requireContext(),
),
confirmButtonTextColor = ThemeManager.resolveAttribute(
R.attr.textOnColorPrimary,
requireContext(),
),
confirmButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(),
),
onConfirmButtonClicked = { _, allowInPrivateBrowsing ->
if (allowInPrivateBrowsing) {
weakApplicationContext.get()?.components?.addonManager?.setAddonAllowedInPrivateBrowsing(
addon,
allowInPrivateBrowsing,
onSuccess = {
runIfFragmentIsAttached {
adapter?.updateAddon(it)
}
},
)
}
},
)
dialog.show(parentFragmentManager, INSTALLATION_DIALOG_FRAGMENT_TAG)
}
}
private val onPositiveButtonClicked: ((Addon) -> Unit) = { addon ->
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.VISIBLE
if (requireContext().settings().accessibilityServicesEnabled) {
binding?.let { announceForAccessibility(it.addonProgressOverlay.addOnsOverlayText.text) }
}
isInstallationInProgress = true
val installOperation = requireContext().components.addonManager.installAddon(
internal fun installAddon(addon: Addon) {
requireContext().components.addonManager.installAddon(
addon,
onSuccess = {
runIfFragmentIsAttached {
isInstallationInProgress = false
adapter?.updateAddon(it)
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
showInstallationDialog(it)
}
},
onError = { _, e ->
this@AddonsManagementFragment.view?.let { view ->
// No need to display an error message if installation was cancelled by the user.
if (e !is CancellationException) {
if (e !is CancellationException && e !is WebExtensionInstallException.UserCancelled) {
val rootView = activity?.getRootView() ?: view
context?.let {
showSnackBar(
@ -415,45 +326,13 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
)
}
}
binding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
isInstallationInProgress = false
}
},
)
binding?.addonProgressOverlay?.cancelButton?.setOnClickListener {
lifecycleScope.launch(Dispatchers.Main) {
val safeBinding = binding
// Hide the installation progress overlay once cancellation is successful.
if (installOperation.cancel().await()) {
safeBinding?.addonProgressOverlay?.overlayCardView?.visibility = View.GONE
}
}
}
}
private fun announceForAccessibility(announcementText: CharSequence) {
val event = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
} else {
@Suppress("DEPRECATION")
AccessibilityEvent.obtain(AccessibilityEvent.TYPE_ANNOUNCEMENT)
}
binding?.addonProgressOverlay?.overlayCardView?.onInitializeAccessibilityEvent(event)
event.text.add(announcementText)
event.contentDescription = null
binding?.addonProgressOverlay?.overlayCardView?.let {
it.parent?.requestSendAccessibilityEvent(
it,
event,
)
}
}
companion object {
private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT"
private const val INSTALLATION_DIALOG_FRAGMENT_TAG = "ADDONS_INSTALLATION_DIALOG_FRAGMENT"
private const val BUNDLE_KEY_INSTALL_EXTERNAL_ADDON_COMPLETE = "INSTALL_EXTERNAL_ADDON_COMPLETE"
}
}

@ -15,7 +15,7 @@ import org.mozilla.fenix.ext.navigateSafe
*/
class AddonsManagementView(
private val navController: NavController,
private val showPermissionDialog: (Addon) -> Unit,
private val onInstallButtonClicked: (Addon) -> Unit,
) : AddonsManagerAdapterDelegate {
override fun onAddonItemClicked(addon: Addon) {
@ -27,7 +27,7 @@ class AddonsManagementView(
}
override fun onInstallAddonButtonClicked(addon: Addon) {
showPermissionDialog(addon)
onInstallButtonClicked(addon)
}
override fun onNotYetSupportedSectionClicked(unsupportedAddons: List<Addon>) {

@ -19,7 +19,9 @@ import androidx.appcompat.view.ContextThemeWrapper
import com.google.android.material.R
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import mozilla.components.concept.base.crash.Breadcrumb
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components
/**
* Base [AppCompatDialogFragment] that adds behaviour to create a top or bottom dialog.
@ -36,6 +38,9 @@ abstract class FenixDialogFragment : AppCompatDialogFragment() {
abstract val layoutId: Int
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb(
Breadcrumb("FenixDialogFragment onCreateDialog Gravity $gravity"),
)
return if (gravity == Gravity.BOTTOM) {
BottomSheetDialog(requireContext(), this.theme).apply {
setOnShowListener {

@ -35,6 +35,8 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch
@ -55,10 +57,12 @@ import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.engine.permission.SitePermissions
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.accounts.FxaCapability
import mozilla.components.feature.accounts.FxaWebChannelFeature
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.app.links.AppLinksFeature
import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature
@ -99,9 +103,7 @@ import mozilla.components.support.ktx.android.view.exitImmersiveMode
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlin.getOrigin
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.support.locale.ActivityContextWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.GleanMetrics.MediaState
@ -116,6 +118,7 @@ import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
import org.mozilla.fenix.components.metrics.MetricsUtils
import org.mozilla.fenix.components.toolbar.BrowserFragmentState
import org.mozilla.fenix.components.toolbar.BrowserFragmentStore
import org.mozilla.fenix.components.toolbar.BrowserToolbarView
@ -134,6 +137,7 @@ import org.mozilla.fenix.downloads.ThirdPartyDownloadDialog
import org.mozilla.fenix.ext.accessibilityManager
import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getFenixAddons
import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.nav
@ -142,6 +146,7 @@ import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.secure
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.extension.WebExtensionPromptFeature
import org.mozilla.fenix.home.HomeScreenViewModel
import org.mozilla.fenix.home.SharedViewModel
import org.mozilla.fenix.perf.MarkersFragmentLifecycleCallbacks
@ -203,6 +208,7 @@ abstract class BaseBrowserFragment :
private val fullScreenFeature = ViewBoundFeatureWrapper<FullScreenFeature>()
private val swipeRefreshFeature = ViewBoundFeatureWrapper<SwipeRefreshFeature>()
private val webchannelIntegration = ViewBoundFeatureWrapper<FxaWebChannelFeature>()
private val webExtensionPromptFeature = ViewBoundFeatureWrapper<WebExtensionPromptFeature>()
private val sitePermissionWifiIntegration =
ViewBoundFeatureWrapper<SitePermissionsWifiIntegration>()
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
@ -544,6 +550,9 @@ abstract class BaseBrowserFragment :
customFirstPartyDownloadDialog = { filename, contentSize, positiveAction, negativeAction ->
run {
if (currentStartDownloadDialog == null) {
context.components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb("FirstPartyDownloadDialog created"),
)
FirstPartyDownloadDialog(
activity = requireActivity(),
filename = filename.value,
@ -551,6 +560,9 @@ abstract class BaseBrowserFragment :
positiveButtonAction = positiveAction.value,
negativeButtonAction = negativeAction.value,
).onDismiss {
context.components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb("FirstPartyDownloadDialog onDismiss"),
)
currentStartDownloadDialog = null
}.show(binding.startDownloadDialogContainer)
.also {
@ -562,12 +574,18 @@ abstract class BaseBrowserFragment :
customThirdPartyDownloadDialog = { downloaderApps, onAppSelected, negativeActionCallback ->
run {
if (currentStartDownloadDialog == null) {
context.components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb("ThirdPartyDownloadDialog created"),
)
ThirdPartyDownloadDialog(
activity = requireActivity(),
downloaderApps = downloaderApps.value,
onAppSelected = onAppSelected.value,
negativeButtonAction = negativeActionCallback.value,
).onDismiss {
context.components.analytics.crashReporter.recordCrashBreadcrumb(
Breadcrumb("ThirdPartyDownloadDialog onDismiss"),
)
currentStartDownloadDialog = null
}.show(binding.startDownloadDialogContainer).also {
currentStartDownloadDialog = it
@ -657,6 +675,7 @@ abstract class BaseBrowserFragment :
store = store,
customTabId = customTabSessionId,
fragmentManager = parentFragmentManager,
tabsUseCases = requireComponents.useCases.tabsUseCases,
creditCardValidationDelegate = DefaultCreditCardValidationDelegate(
context.components.core.lazyAutofillStorage,
),
@ -858,7 +877,7 @@ abstract class BaseBrowserFragment :
store.flowScoped(viewLifecycleOwner) { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) }
.ifChanged { tab -> tab.content.pictureInPictureEnabled }
.distinctUntilChangedBy { tab -> tab.content.pictureInPictureEnabled }
.collect { tab -> pipModeChanged(tab) }
}
@ -894,6 +913,17 @@ abstract class BaseBrowserFragment :
view = view,
)
webExtensionPromptFeature.set(
feature = WebExtensionPromptFeature(
store = requireComponents.core.store,
provideAddons = ::provideAddons,
context = requireContext(),
fragmentManager = parentFragmentManager,
view = view,
),
owner = this,
view = view,
)
initializeEngineView(toolbarHeight)
}
@ -969,7 +999,7 @@ abstract class BaseBrowserFragment :
}
create()
}.show().withCenterAlignedButtons().secure(activity)
}.show().secure(activity)
context.settings().incrementSecureWarningCount()
}
@ -1112,7 +1142,7 @@ abstract class BaseBrowserFragment :
val activity = activity as HomeActivity
consumeFlow(store) { flow ->
flow.map { state -> state.restoreComplete }
.ifChanged()
.distinctUntilChanged()
.collect { restored ->
if (restored) {
// Once tab restoration is complete, if there are no tabs to show in the browser, go home
@ -1131,7 +1161,7 @@ abstract class BaseBrowserFragment :
@VisibleForTesting
internal fun observeTabSelection(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.ifChanged {
flow.distinctUntilChangedBy {
it.selectedTabId
}
.mapNotNull {
@ -1387,6 +1417,7 @@ abstract class BaseBrowserFragment :
position = null,
)
MetricsUtils.recordBookmarkMetrics(MetricsUtils.BookmarkAction.ADD, METRIC_SOURCE)
withContext(Main) {
view?.let {
FenixSnackbar.make(
@ -1396,6 +1427,10 @@ abstract class BaseBrowserFragment :
)
.setText(getString(R.string.bookmark_saved_snackbar))
.setAction(getString(R.string.edit_bookmark_snackbar_action)) {
MetricsUtils.recordBookmarkMetrics(
MetricsUtils.BookmarkAction.EDIT,
TOAST_METRIC_SOURCE,
)
nav(
R.id.browserFragment,
BrowserFragmentDirections.actionGlobalBookmarkEditFragment(
@ -1550,6 +1585,8 @@ abstract class BaseBrowserFragment :
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val REQUEST_CODE_APP_PERMISSIONS = 3
private const val METRIC_SOURCE = "page_action_menu"
private const val TOAST_METRIC_SOURCE = "add_bookmark_toast"
val onboardingLinksList: List<String> = listOf(
SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE),
@ -1605,4 +1642,13 @@ abstract class BaseBrowserFragment :
return isValidStatus && isSameTab
}
private suspend fun provideAddons(): List<Addon> {
return withContext(IO) {
// We deactivated the cache to get the most up-to-date list of add-ons to match against.
// as this will be used to install add-ons from AMO.
val addons = requireContext().components.addonManager.getFenixAddons(allowCache = false)
addons
}
}
}

@ -50,6 +50,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.dialog.CookieBannerReEngagementDialogUtils
import org.mozilla.fenix.settings.quicksettings.protections.cookiebanners.getCookieBannerUIMode
import org.mozilla.fenix.shopping.ReviewQualityCheckFeature
import org.mozilla.fenix.shortcut.PwaOnboardingObserver
import org.mozilla.fenix.theme.ThemeManager
@ -61,8 +62,10 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val reviewQualityCheckFeature = ViewBoundFeatureWrapper<ReviewQualityCheckFeature>()
private var readerModeAvailable = false
private var reviewQualityCheckAvailable = false
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
private var forwardAction: BrowserToolbar.TwoStateButton? = null
@ -118,7 +121,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
contentDescription = context.getString(R.string.browser_menu_read),
contentDescriptionSelected = context.getString(R.string.browser_menu_read_close),
visible = {
readerModeAvailable
readerModeAvailable && !reviewQualityCheckAvailable
},
selected = getCurrentTab()?.let {
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
@ -128,6 +131,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
browserToolbarView.view.addPageAction(readerModeAction)
initReviewQualityCheck(context, view)
thumbnailsFeature.set(
feature = BrowserThumbnails(context, binding.engineView, components.core.store),
owner = this,
@ -185,6 +190,38 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
}
private fun initReviewQualityCheck(context: Context, view: View) {
val reviewQualityCheck =
BrowserToolbar.ToggleButton(
image = AppCompatResources.getDrawable(
context,
R.drawable.ic_shopping_cart,
)!!,
imageSelected = AppCompatResources.getDrawable(
context,
R.drawable.ic_shopping_cart,
)!!,
contentDescription = context.getString(R.string.browser_menu_review_quality_check),
contentDescriptionSelected = context.getString(R.string.browser_menu_review_quality_check_close),
visible = { reviewQualityCheckAvailable },
listener = {
findNavController().navigate(
BrowserFragmentDirections.actionBrowserFragmentToReviewQualityCheckDialogFragment(),
)
},
)
browserToolbarView.view.addPageAction(reviewQualityCheck)
reviewQualityCheckFeature.set(
feature = ReviewQualityCheckFeature(
onAvailabilityChange = { reviewQualityCheckAvailable = it },
),
owner = this,
view = view,
)
}
override fun onUpdateToolbarForConfigurationChange(toolbar: BrowserToolbarView) {
super.onUpdateToolbarForConfigurationChange(toolbar)

@ -67,12 +67,12 @@ class SwipeGestureLayout @JvmOverloads constructor(
}
override fun onScroll(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float,
): Boolean {
val start = e1.let { event -> PointF(event.rawX, event.rawY) }
val start = e1?.let { event -> PointF(event.rawX, event.rawY) } ?: return false
val next = e2.let { event -> PointF(event.rawX, event.rawY) }
if (activeListener == null && !handledInitialScroll) {
@ -86,7 +86,7 @@ class SwipeGestureLayout @JvmOverloads constructor(
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float,

@ -7,8 +7,8 @@ package org.mozilla.fenix.browser.readermode
import android.view.View
import android.widget.Button
import android.widget.RadioButton
import androidx.appcompat.content.res.AppCompatResources
import androidx.annotation.VisibleForTesting
import androidx.appcompat.content.res.AppCompatResources
import mozilla.components.feature.readerview.ReaderViewFeature
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import org.mozilla.fenix.R

@ -72,7 +72,7 @@ class CollectionCreationView(
interactor.onNewCollectionNameSaved(selectedTabs.toList(), text)
SaveCollectionStep.RenameCollection ->
selectedCollection?.let { interactor.onCollectionRenamed(it, text) }
else -> { /* noop */
else -> { // noop
}
}
}
@ -243,7 +243,7 @@ class CollectionCreationView(
}
transition.addListener(
object : Transition.TransitionListener {
override fun onTransitionStart(transition: Transition) { /* noop */
override fun onTransitionStart(transition: Transition) { // noop
}
override fun onTransitionEnd(transition: Transition) {
@ -251,13 +251,13 @@ class CollectionCreationView(
transition.removeListener(this)
}
override fun onTransitionCancel(transition: Transition) { /* noop */
override fun onTransitionCancel(transition: Transition) { // noop
}
override fun onTransitionPause(transition: Transition) { /* noop */
override fun onTransitionPause(transition: Transition) { // noop
}
override fun onTransitionResume(transition: Transition) { /* noop */
override fun onTransitionResume(transition: Transition) { // noop
}
},
)

@ -15,7 +15,6 @@ import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.showKeyboard
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.R
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.getDefaultCollectionNumber
@ -80,7 +79,7 @@ fun CollectionsDialog.show(
dialog.cancel()
}
val dialog = builder.create().withCenterAlignedButtons()
val dialog = builder.create()
val collectionNames =
arrayOf(context.getString(R.string.tab_tray_add_new_collection)) + collections
val collectionsListAdapter = CollectionsListAdapter(collectionNames) {
@ -127,7 +126,7 @@ internal fun CollectionsDialog.showAddNewDialog(
onNegativeButtonClick.invoke()
dialog.cancel()
}
.create().withCenterAlignedButtons()
.create()
.show()
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)

@ -18,6 +18,7 @@ import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.messaging.FxNimbusMessaging
import mozilla.components.service.nimbus.messaging.NimbusMessagingStorage
import mozilla.components.service.nimbus.messaging.OnDiskMessageMetadataStorage
import mozilla.components.support.utils.BrowsersCache
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
@ -29,12 +30,12 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.InstallReferrerMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.metrics.MetricsStorage
import org.mozilla.fenix.crashes.CrashFactCollector
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.messaging.CustomAttributeProvider
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.BrowsersCache
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VERSION
@ -120,6 +121,10 @@ class Analytics(
)
}
val crashFactCollector: CrashFactCollector by lazyMonitored {
CrashFactCollector(crashReporter)
}
val metricsStorage: MetricsStorage by lazyMonitored {
DefaultMetricsStorage(
context = context,

@ -52,6 +52,7 @@ import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.recentlyclosed.RecentlyClosedTabsStorage
import mozilla.components.feature.search.ext.createApplicationSearchEngine
import mozilla.components.feature.search.middleware.AdsTelemetryMiddleware
import mozilla.components.feature.search.middleware.SearchExtraParams
import mozilla.components.feature.search.middleware.SearchMiddleware
import mozilla.components.feature.search.region.RegionMiddleware
import mozilla.components.feature.search.telemetry.ads.AdsTelemetry
@ -95,6 +96,7 @@ import org.mozilla.fenix.historymetadata.DefaultHistoryMetadataService
import org.mozilla.fenix.historymetadata.HistoryMetadataMiddleware
import org.mozilla.fenix.historymetadata.HistoryMetadataService
import org.mozilla.fenix.media.MediaSessionService
import org.mozilla.fenix.nimbus.FxNimbus
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.settings.SupportUtils
@ -257,9 +259,17 @@ class Core(
UndoMiddleware(context.getUndoDelay()),
RegionMiddleware(context, locationService),
SearchMiddleware(
context,
context = context,
additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context),
searchExtraParams =
FxNimbus.features.searchExtraParams.value().searchNameChannelId
.firstNotNullOfOrNull {
SearchExtraParams(
it.key,
it.value,
)
},
),
RecordingDevicesMiddleware(context, context.components.notificationsDelegate),
PromptMiddleware(),

@ -76,7 +76,7 @@ class FenixSnackbar private constructor(
companion object {
const val LENGTH_LONG = Snackbar.LENGTH_LONG
const val LENGTH_SHORT = Snackbar.LENGTH_SHORT
private 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

@ -24,8 +24,8 @@ class PermissionStorage(
* Persists the [sitePermissions] provided as a parameter.
* @param sitePermissions the [sitePermissions] to be stored.
*/
suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.save(sitePermissions, private = false)
suspend fun add(sitePermissions: SitePermissions, private: Boolean) = withContext(dispatcher) {
permissionsStorage.save(sitePermissions, private = private)
}
/**

@ -106,9 +106,9 @@ class TrackingProtectionPolicyFactory(
}
}
@Suppress("MaxLineLength")
@VisibleForTesting
internal fun TrackingProtectionPolicyForSessionTypes.applyTCPIfNeeded(settings: Settings):
TrackingProtectionPolicyForSessionTypes {
internal fun TrackingProtectionPolicyForSessionTypes.applyTCPIfNeeded(settings: Settings): TrackingProtectionPolicyForSessionTypes {
val updatedCookiePolicy = if (settings.enabledTotalCookieProtection) {
CookiePolicy.ACCEPT_FIRST_PARTY_AND_ISOLATE_OTHERS
} else {

@ -15,7 +15,7 @@ import org.mozilla.fenix.library.history.History
import org.mozilla.fenix.library.history.HistoryItemTimeGroup
import org.mozilla.fenix.utils.Settings.Companion.SEARCH_GROUP_MINIMUM_SITES
private const val BUFFER_TIME = 15000 /* 15 seconds in ms */
private const val BUFFER_TIME = 15000 // 15 seconds in ms
/**
* Class representing a history entry.

@ -39,8 +39,13 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
val installationPing = FirstSessionPing(application)
FirstSession.adjustAttributionTimespan.start()
val timerId = FirstSession.adjustAttributionTime.start()
config.setOnAttributionChangedListener {
if (!installationPing.wasAlreadyTriggered()) {
FirstSession.adjustAttributionTimespan.stop()
}
FirstSession.adjustAttributionTime.stopAndAccumulate(timerId)
if (!it.network.isNullOrEmpty()) {
application.applicationContext.settings().adjustNetwork =
@ -69,6 +74,7 @@ class AdjustMetricsService(private val application: Application) : MetricsServic
}
override fun stop() {
FirstSession.adjustAttributionTimespan.cancel()
Adjust.setEnabled(false)
Adjust.gdprForgetMe(application.applicationContext)
}

@ -35,7 +35,6 @@ class FirstSessionPing(private val context: Context) {
*
* @return true if it was already triggered, false otherwise.
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun wasAlreadyTriggered(): Boolean {
return prefs.getBoolean("ping_sent", false)
}

@ -58,6 +58,30 @@ object MetricsUtils {
Events.performedSearch.record(Events.PerformedSearchExtra(performedSearchExtra))
}
/**
* Records the appropriate metric for performed Bookmark action.
* @param action The [BookmarkAction] being counted.
* @param source Describes where the action was called from.
*/
fun recordBookmarkMetrics(
action: BookmarkAction,
source: String,
) {
when (action) {
BookmarkAction.ADD -> Metrics.bookmarksAdd[source].add()
BookmarkAction.EDIT -> Metrics.bookmarksEdit[source].add()
BookmarkAction.DELETE -> Metrics.bookmarksDelete[source].add()
BookmarkAction.OPEN -> Metrics.bookmarksOpen[source].add()
}
}
/**
* Describes which bookmark action is being recorded.
*/
enum class BookmarkAction {
ADD, EDIT, DELETE, OPEN
}
/**
* Get the default salt to use for hashing. This is a convenience
* function to help with unit tests.

@ -6,8 +6,8 @@ package org.mozilla.fenix.components.metrics
import android.content.Context
import android.content.pm.PackageManager
import mozilla.components.support.utils.BrowsersCache
import mozilla.components.support.utils.ext.getPackageInfoCompat
import org.mozilla.fenix.utils.BrowsersCache
object MozillaProductDetector {
enum class MozillaProducts(val productName: String) {

@ -15,6 +15,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
@ -28,7 +29,7 @@ import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.top.sites.TopSite
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.ui.widgets.withCenterAlignedButtons
import org.mozilla.fenix.GleanMetrics.AppMenu
import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.ReaderMode
@ -49,7 +50,6 @@ import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.utils.Do
import org.mozilla.fenix.utils.Settings
/**
@ -95,7 +95,7 @@ class DefaultBrowserToolbarMenuController(
val customTabUseCases = activity.components.useCases.customTabsUseCases
trackToolbarItemInteraction(item)
Do exhaustive when (item) {
when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.InstallPwaToHomeScreen -> {
@ -266,7 +266,7 @@ class DefaultBrowserToolbarMenuController(
setPositiveButton(R.string.top_sites_max_limit_confirmation_button) { dialog, _ ->
dialog.dismiss()
}
create().withCenterAlignedButtons()
create()
}.show()
} else {
ioScope.launch {
@ -333,6 +333,11 @@ class DefaultBrowserToolbarMenuController(
navController.nav(R.id.browserFragment, directions)
}
}
is ToolbarMenu.Item.PrintContent -> {
store.state.selectedTab?.let {
store.dispatch(EngineAction.PrintContentAction(it.id))
}
}
is ToolbarMenu.Item.Bookmark -> {
store.state.selectedTab?.let {
getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) }
@ -443,10 +448,14 @@ class DefaultBrowserToolbarMenuController(
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("save_to_collection"))
is ToolbarMenu.Item.AddToTopSites ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("add_to_top_sites"))
is ToolbarMenu.Item.PrintContent ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("print_content"))
is ToolbarMenu.Item.AddToHomeScreen ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("add_to_homescreen"))
is ToolbarMenu.Item.SyncAccount ->
is ToolbarMenu.Item.SyncAccount -> {
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("sync_account"))
AppMenu.signIntoSync.add()
}
is ToolbarMenu.Item.Bookmark ->
Events.browserMenuAction.record(Events.BrowserMenuActionExtra("bookmark"))
is ToolbarMenu.Item.AddonsManager ->

@ -35,6 +35,7 @@ import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.ext.components
@ -301,6 +302,14 @@ open class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
private val printPageItem = BrowserMenuImageText(
label = context.getString(R.string.menu_print),
imageResource = R.drawable.ic_print,
iconTintColorResource = primaryTextColor(),
) {
onItemTapped.invoke(ToolbarMenu.Item.PrintContent)
}
@VisibleForTesting
internal val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
@ -381,6 +390,7 @@ open class DefaultToolbarMenu(
installToHomescreen.apply { visible = ::canInstall },
addRemoveTopSitesItem,
saveToCollectionItem,
if (FeatureFlags.print) printPageItem else null,
BrowserMenuDivider(),
settingsItem,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,

@ -20,6 +20,11 @@ interface ToolbarMenu {
object Stop : Item()
object OpenInFenix : Item()
object SaveToCollection : Item()
/**
* Prints the currently displayed page content.
*/
object PrintContent : Item()
object AddToTopSites : Item()
object RemoveFromTopSites : Item()
object InstallPwaToHomeScreen : Item()

@ -0,0 +1,74 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.Color
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* A handle present on top of a bottom sheet. This is selectable when talkback is enabled.
*
* @param onRequestDismiss Invoked on clicking the handle when talkback is enabled.
* @param contentDescription Content Description of the composable.
* @param modifier The modifier to be applied to the Composable.
* @param color Color of the handle.
*/
@Composable
fun BottomSheetHandle(
onRequestDismiss: () -> Unit,
contentDescription: String,
modifier: Modifier = Modifier,
color: Color = FirefoxTheme.colors.textSecondary,
) {
Canvas(
modifier = modifier
.height(dimensionResource(id = R.dimen.bottom_sheet_handle_height))
.semantics(mergeDescendants = true) {
this.contentDescription = contentDescription
onClick {
onRequestDismiss()
true
}
},
) {
drawRect(color = color)
}
}
@Composable
@LightDarkPreview
private fun BottomSheetHandlePreview() {
FirefoxTheme {
Column(
modifier = Modifier
.background(color = FirefoxTheme.colors.layer1)
.padding(16.dp),
) {
BottomSheetHandle(
onRequestDismiss = {},
contentDescription = "",
modifier = Modifier
.width(100.dp)
.align(Alignment.CenterHorizontally),
)
}
}
}

@ -83,8 +83,8 @@ fun ClickableSubstringLink(
annotatedText
.getStringAnnotations("link", it, it)
.firstOrNull()?.let {
onClick()
}
onClick()
}
},
)
}

@ -28,6 +28,9 @@ import androidx.compose.ui.unit.sp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
const val ITEM_WIDTH = 328
const val ITEM_HEIGHT = 116
/**
* Default layout of a large tab shown in a list taking String arguments for title and caption.
* Has the following structure:
@ -138,7 +141,7 @@ fun ListItemTabSurface(
onClick: (() -> Unit)? = null,
tabDetails: @Composable () -> Unit,
) {
var modifier = Modifier.size(328.dp, 116.dp)
var modifier = Modifier.size(ITEM_WIDTH.dp, ITEM_HEIGHT.dp)
if (onClick != null) modifier = modifier.then(Modifier.clickable { onClick() })
Card(

@ -46,7 +46,7 @@ fun ListItemTabLargePlaceholder(
) {
Card(
modifier = Modifier
.size(328.dp, 116.dp)
.size(ITEM_WIDTH.dp, ITEM_HEIGHT.dp)
.clickable { onClick() },
shape = RoundedCornerShape(8.dp),
backgroundColor = FirefoxTheme.colors.layer2,

@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
@ -43,6 +44,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param inactiveColor The color of page indicators that are inactive.
* @param leaveTrail Whether to leave the trail of indicators to show progress.
* This defaults to false and just shows the current one as active.
* @param spacing The spacing between each pager indicator in [Dp].
*/
@Composable
fun PagerIndicator(
@ -52,10 +54,11 @@ fun PagerIndicator(
activeColor: Color = FirefoxTheme.colors.indicatorActive,
inactiveColor: Color = FirefoxTheme.colors.indicatorInactive,
leaveTrail: Boolean = false,
spacing: Dp = 8.dp,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(spacing),
verticalAlignment = Alignment.CenterVertically,
) {
val showActiveModifier: (pageIndex: Int) -> Boolean =

@ -0,0 +1,169 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.material.DismissDirection
import androidx.compose.material.DismissDirection.EndToStart
import androidx.compose.material.DismissDirection.StartToEnd
import androidx.compose.material.DismissState
import androidx.compose.material.DismissValue
import androidx.compose.material.DismissValue.Default
import androidx.compose.material.DismissValue.DismissedToEnd
import androidx.compose.material.DismissValue.DismissedToStart
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.FixedThreshold
import androidx.compose.material.FractionalThreshold
import androidx.compose.material.Text
import androidx.compose.material.ThresholdConfig
import androidx.compose.material.rememberDismissState
import androidx.compose.material.swipeable
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.mozilla.fenix.theme.FirefoxTheme
import kotlin.math.roundToInt
/**
* A composable that can be dismissed by swiping left or right
*
* @param state The state of this component.
* @param modifier Optional [Modifier] for this component.
* @param enabled [Boolean] controlling whether the content is swipeable or not.
* @param directions The set of directions in which the component can be dismissed.
* @param dismissThreshold The threshold the item needs to be swiped in order to be dismissed.
* @param backgroundContent A composable that is stacked behind the primary content and is exposed
* when the content is swiped. You can/should use the [state] to have different backgrounds on each side.
* @param dismissContent The content that can be dismissed.
*/
@Composable
@ExperimentalMaterialApi
fun SwipeToDismiss(
state: DismissState,
modifier: Modifier = Modifier,
enabled: Boolean = true,
directions: Set<DismissDirection> = setOf(EndToStart, StartToEnd),
dismissThreshold: ThresholdConfig = FractionalThreshold(DISMISS_THRESHOLD),
backgroundContent: @Composable RowScope.() -> Unit,
dismissContent: @Composable RowScope.() -> Unit,
) {
val swipeWidth = with(LocalDensity.current) {
LocalConfiguration.current.screenWidthDp.dp.toPx()
}
val anchors = mutableMapOf(0f to Default)
val thresholds = { _: DismissValue, _: DismissValue ->
dismissThreshold
}
if (StartToEnd in directions) anchors += swipeWidth to DismissedToEnd
if (EndToStart in directions) anchors += -swipeWidth to DismissedToStart
Box(
Modifier
.swipeable(
state = state,
anchors = anchors,
thresholds = thresholds,
orientation = Orientation.Horizontal,
enabled = state.currentValue == Default && enabled,
reverseDirection = LocalLayoutDirection.current == LayoutDirection.Rtl,
resistance = null,
)
.then(modifier),
) {
Row(
content = backgroundContent,
modifier = Modifier.matchParentSize(),
)
Row(
content = dismissContent,
modifier = Modifier.offset { IntOffset(state.offset.value.roundToInt(), 0) },
)
}
}
private const val DISMISS_THRESHOLD = 0.5f
@OptIn(ExperimentalMaterialApi::class)
@Composable
private fun SwipeablePreview(directions: Set<DismissDirection>, text: String, threshold: ThresholdConfig) {
val state = rememberDismissState()
Box(
modifier = Modifier
.height(30.dp)
.fillMaxWidth(),
) {
SwipeToDismiss(
state = state,
directions = directions,
dismissThreshold = threshold,
backgroundContent = {
Box(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layerAccent),
)
},
) {
Row(
modifier = Modifier
.fillMaxSize()
.background(FirefoxTheme.colors.layer1),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
Text(text)
}
}
}
}
@Suppress("MagicNumber")
@OptIn(ExperimentalMaterialApi::class)
@Composable
@Preview
private fun SwipeToDismissPreview() {
FirefoxTheme {
Column {
SwipeablePreview(
directions = setOf(StartToEnd),
text = "Swipe to right 50% ->",
FractionalThreshold(.5f),
)
Spacer(Modifier.height(30.dp))
SwipeablePreview(
directions = setOf(EndToStart),
text = "<- Swipe to left 100%",
FractionalThreshold(1f),
)
Spacer(Modifier.height(30.dp))
SwipeablePreview(
directions = setOf(StartToEnd, EndToStart),
text = "<- Swipe both ways 20dp ->",
FixedThreshold(20.dp),
)
}
}
}

@ -0,0 +1,101 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
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
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import org.mozilla.fenix.theme.FirefoxTheme
private const val THUMBNAIL_SIZE = 108
private const val FALLBACK_ICON_SIZE = 36
/**
* Thumbnail belonging to a [tab]. If a thumbnail is not available, the favicon
* will be displayed until the thumbnail is loaded.
*
* @param tab The given [TabSessionState] to render a thumbnail for.
* @param size [Dp] size of the thumbnail.
* @param backgroundColor [Color] used for the background of the favicon.
* @param modifier [Modifier] used to draw the image content.
* @param contentDescription Text used by accessibility services
* to describe what this image represents.
* @param contentScale [ContentScale] used to draw image content.
* @param alignment [Alignment] used to draw the image content.
*/
@Composable
@Suppress("LongParameterList")
fun TabThumbnail(
tab: TabSessionState,
modifier: Modifier = Modifier,
size: Dp = THUMBNAIL_SIZE.dp,
backgroundColor: Color = FirefoxTheme.colors.layer2,
contentDescription: String? = null,
contentScale: ContentScale = ContentScale.FillWidth,
alignment: Alignment = Alignment.TopCenter,
) {
Card(
modifier = modifier,
backgroundColor = backgroundColor,
) {
ThumbnailImage(
key = tab.id,
size = size,
modifier = modifier,
contentScale = contentScale,
alignment = alignment,
) {
Box(
modifier = Modifier.size(FALLBACK_ICON_SIZE.dp),
contentAlignment = Alignment.Center,
) {
val icon = tab.content.icon
if (icon != null) {
icon.prepareToDraw()
Image(
bitmap = icon.asImageBitmap(),
contentDescription = contentDescription,
modifier = Modifier
.size(FALLBACK_ICON_SIZE.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = contentScale,
)
} else {
Favicon(
url = tab.content.url,
size = FALLBACK_ICON_SIZE.dp,
)
}
}
}
}
}
@Preview
@Composable
private fun ThumbnailCardPreview() {
FirefoxTheme {
TabThumbnail(
tab = createTab(url = "www.mozilla.com", title = "Mozilla"),
modifier = Modifier
.size(THUMBNAIL_SIZE.dp, 80.dp)
.clip(RoundedCornerShape(8.dp)),
)
}
}

@ -11,28 +11,23 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import mozilla.components.browser.icons.compose.Loader
import mozilla.components.browser.icons.compose.Placeholder
import mozilla.components.browser.icons.compose.WithIcon
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme
private const val THUMBNAIL_SIZE = 108
private const val FALLBACK_ICON_SIZE = 36
/**
* Card which will display a thumbnail. If a thumbnail is not available for [url], the favicon
* will be displayed until the thumbnail is loaded.
@ -51,7 +46,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
fun ThumbnailCard(
url: String,
key: String,
size: Dp = 108.dp,
size: Dp = THUMBNAIL_SIZE.dp,
backgroundColor: Color = FirefoxTheme.colors.layer2,
modifier: Modifier = Modifier,
contentDescription: String? = null,
@ -62,76 +57,38 @@ fun ThumbnailCard(
modifier = modifier,
backgroundColor = backgroundColor,
) {
if (inComposePreview) {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer3),
)
} else {
ThumbnailImage(
key = key,
size = size,
modifier = modifier,
contentScale = contentScale,
alignment = alignment,
) {
components.core.icons.Loader(url) {
Placeholder {
Box(
modifier = Modifier.background(color = FirefoxTheme.colors.layer3),
)
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer3))
}
WithIcon { icon ->
Box(
modifier = Modifier.size(36.dp),
modifier = Modifier.size(FALLBACK_ICON_SIZE.dp),
contentAlignment = Alignment.Center,
) {
Image(
painter = icon.painter,
contentDescription = contentDescription,
modifier = Modifier
.size(36.dp)
.size(FALLBACK_ICON_SIZE.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = contentScale,
)
}
}
}
ThumbnailImage(
key = key,
size = size,
modifier = modifier,
contentScale = contentScale,
alignment = alignment,
)
}
}
}
@Composable
private fun ThumbnailImage(
key: String,
size: Dp,
modifier: Modifier,
contentScale: ContentScale,
alignment: Alignment,
) {
val rememberBitmap = remember(key) { mutableStateOf<ImageBitmap?>(null) }
val thumbnailSize = LocalDensity.current.run { size.toPx().toInt() }
val request = ImageLoadRequest(key, thumbnailSize)
val storage = components.core.thumbnailStorage
val bitmap = rememberBitmap.value
LaunchedEffect(key) {
rememberBitmap.value = storage.loadThumbnail(request).await()?.asImageBitmap()
}
if (bitmap != null) {
val painter = BitmapPainter(bitmap)
Image(
painter = painter,
contentDescription = null,
modifier = modifier,
contentScale = contentScale,
alignment = alignment,
)
}
}
@Preview
@Composable
private fun ThumbnailCardPreview() {
@ -140,7 +97,7 @@ private fun ThumbnailCardPreview() {
url = "https://mozilla.com",
key = "123",
modifier = Modifier
.size(108.dp, 80.dp)
.size(THUMBNAIL_SIZE.dp)
.clip(RoundedCornerShape(8.dp)),
)
}

@ -0,0 +1,124 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import mozilla.components.concept.base.images.ImageLoadRequest
import org.mozilla.fenix.components.components
import org.mozilla.fenix.theme.FirefoxTheme
/**
* Thumbnail belonging to a [key]. Asynchronously fetches the bitmap from storage.
*
* @param key Key used to remember the thumbnail for future compositions.
* @param size [Dp] size of the thumbnail.
* @param modifier [Modifier] used to draw the image content.
* @param contentScale [ContentScale] used to draw image content.
* @param alignment [Alignment] used to draw the image content.
*/
@Composable
@Suppress("LongParameterList")
fun ThumbnailImage(
key: String,
size: Dp,
modifier: Modifier,
contentScale: ContentScale,
alignment: Alignment,
fallbackContent: @Composable () -> Unit,
) {
if (inComposePreview) {
Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer3))
} else {
val thumbnailSize = LocalDensity.current.run { size.toPx().toInt() }
val request = ImageLoadRequest(key, thumbnailSize)
val storage = components.core.thumbnailStorage
var state by remember { mutableStateOf(ThumbnailImageState(null, false)) }
val scope = rememberCoroutineScope()
DisposableEffect(Unit) {
if (!state.hasLoaded) {
scope.launch {
val thumbnailBitmap = storage.loadThumbnail(request).await()
thumbnailBitmap?.prepareToDraw()
state = ThumbnailImageState(
bitmap = thumbnailBitmap,
hasLoaded = true,
)
}
}
onDispose {
// Recycle the bitmap to liberate the RAM. Without this, a list of [ThumbnailImage]
// will bloat the memory. This is a trade-off, however, as the bitmap
// will be re-fetched if this Composable is disposed and re-loaded.
state.bitmap?.recycle()
state = ThumbnailImageState(
bitmap = null,
hasLoaded = false,
)
}
}
if (state.bitmap == null && state.hasLoaded) {
fallbackContent()
} else {
state.bitmap?.let { bitmap ->
Image(
painter = BitmapPainter(bitmap.asImageBitmap()),
contentDescription = null,
modifier = modifier,
contentScale = contentScale,
alignment = alignment,
)
}
}
}
}
/**
* State wrapper for [ThumbnailImage].
*/
private data class ThumbnailImageState(
val bitmap: Bitmap?,
val hasLoaded: Boolean,
)
/**
* This preview does not demo anything. This is to ensure that [ThumbnailImage] does not break other previews.
*/
@Preview
@Composable
private fun ThumbnailImagePreview() {
FirefoxTheme {
ThumbnailImage(
key = "",
size = 1.dp,
modifier = Modifier,
contentScale = ContentScale.Crop,
alignment = Alignment.Center,
fallbackContent = {},
)
}
}

@ -5,6 +5,7 @@
package org.mozilla.fenix.compose.list
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@ -86,6 +87,7 @@ fun TextListItem(
*
* @param label The label in the list item.
* @param description An optional description text below the label.
* @param faviconPainter Optional painter to use when fetching a new favicon is unnecessary.
* @param onClick Called when the user clicks on the item.
* @param url Website [url] for which the favicon will be shown.
* @param iconPainter [Painter] used to display an [IconButton] after the list item.
@ -96,6 +98,7 @@ fun TextListItem(
fun FaviconListItem(
label: String,
description: String? = null,
faviconPainter: Painter? = null,
onClick: (() -> Unit)? = null,
url: String,
iconPainter: Painter? = null,
@ -107,11 +110,21 @@ fun FaviconListItem(
description = description,
onClick = onClick,
beforeListAction = {
Favicon(
url = url,
size = ICON_SIZE,
modifier = Modifier.padding(horizontal = 16.dp),
)
if (faviconPainter != null) {
Image(
painter = faviconPainter,
contentDescription = null,
modifier = Modifier
.padding(horizontal = 16.dp)
.size(ICON_SIZE),
)
} else {
Favicon(
url = url,
size = ICON_SIZE,
modifier = Modifier.padding(horizontal = 16.dp),
)
}
},
afterListAction = {
if (iconPainter != null && onIconClick != null) {
@ -325,7 +338,7 @@ private fun IconListItemWithRightIconPreview() {
)
private fun FaviconListItemPreview() {
FirefoxTheme {
Box(Modifier.background(FirefoxTheme.colors.layer1)) {
Column(Modifier.background(FirefoxTheme.colors.layer1)) {
FaviconListItem(
label = "Favicon + right icon + clicks",
description = "Description text",
@ -334,6 +347,14 @@ private fun FaviconListItemPreview() {
iconPainter = painterResource(R.drawable.ic_menu),
onIconClick = { println("icon click") },
)
FaviconListItem(
label = "Favicon + painter",
description = "Description text",
faviconPainter = painterResource(id = R.drawable.ic_tab_collection),
onClick = { println("list item click") },
url = "",
)
}
}
}

@ -0,0 +1,112 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.compose.tabstray
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DismissDirection
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import mozilla.components.feature.tab.collections.Tab
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.theme.FirefoxTheme
/**
* The background of a [Tab] that is being swiped left or right.
*
* @param dismissDirection [DismissDirection] of the ongoing swipe. Depending on the direction,
* the background will also include a warning icon at the start of the swipe gesture.
* If `null` the warning icon will be shown at both ends.
* @param shape Shape of the background.
*/
@Composable
fun DismissedTabBackground(
dismissDirection: DismissDirection?,
shape: Shape,
) {
Card(
modifier = Modifier.fillMaxSize(),
backgroundColor = FirefoxTheme.colors.layer3,
shape = shape,
elevation = 0.dp,
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
painter = painterResource(R.drawable.ic_delete),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 32.dp)
// Only show the delete icon for where the swipe starts.
.alpha(
if (dismissDirection == DismissDirection.StartToEnd || dismissDirection == null) 1f else 0f,
),
tint = FirefoxTheme.colors.iconWarning,
)
Icon(
painter = painterResource(R.drawable.ic_delete),
contentDescription = null,
modifier = Modifier
.padding(horizontal = 32.dp)
// Only show the delete icon for where the swipe starts.
.alpha(
if (dismissDirection == DismissDirection.EndToStart || dismissDirection == null) 1f else 0f,
),
tint = FirefoxTheme.colors.iconWarning,
)
}
}
}
@Composable
@LightDarkPreview
private fun DismissedTabBackgroundPreview() {
FirefoxTheme {
Column {
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(
dismissDirection = DismissDirection.StartToEnd,
shape = RoundedCornerShape(0.dp),
)
}
Spacer(Modifier.height(10.dp))
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(
dismissDirection = DismissDirection.EndToStart,
shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
)
}
Spacer(Modifier.height(10.dp))
Box(modifier = Modifier.height(56.dp)) {
DismissedTabBackground(
dismissDirection = null,
shape = RoundedCornerShape(0.dp),
)
}
}
}
}

@ -7,6 +7,7 @@ package org.mozilla.fenix.compose.tabstray
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
@ -28,12 +29,14 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param tab [TabSessionState] which the image should be shown.
* @param onMediaIconClicked handles the click event when tab has media session like play/pause.
* @param modifier [Modifier] to be applied to the layout.
* @param interactionSource [MutableInteractionSource] used to propagate the ripple effect on click.
*/
@Composable
fun MediaImage(
tab: TabSessionState,
onMediaIconClicked: ((TabSessionState) -> Unit),
modifier: Modifier,
interactionSource: MutableInteractionSource = MutableInteractionSource(),
) {
val (icon, contentDescription) = when (tab.mediaSessionState?.playbackState) {
PlaybackState.PAUSED -> {
@ -49,7 +52,10 @@ fun MediaImage(
Image(
painter = rememberDrawablePainter(drawable = drawable),
contentDescription = stringResource(contentDescription),
modifier = modifier.clickable { onMediaIconClicked(tab) },
modifier = modifier.clickable(
interactionSource = interactionSource,
indication = null,
) { onMediaIconClicked(tab) },
)
}

@ -6,30 +6,41 @@ package org.mozilla.fenix.compose.tabstray
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.rememberDismissState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
@ -42,15 +53,17 @@ import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import androidx.core.text.BidiFormatter
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.Divider
import org.mozilla.fenix.compose.Favicon
import org.mozilla.fenix.compose.HorizontalFadingEdgeBox
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.SwipeToDismiss
import org.mozilla.fenix.compose.TabThumbnail
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.tabstray.TabsTrayTestTag
import org.mozilla.fenix.tabstray.ext.toDisplayTitle
@ -71,7 +84,7 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onClick Callback to handle when item is clicked.
* @param onLongClick Callback to handle when item is long clicked.
*/
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
@Suppress("MagicNumber", "LongParameterList", "LongMethod")
fun TabGridItem(
@ -84,7 +97,7 @@ fun TabGridItem(
onClick: (tab: TabSessionState) -> Unit,
onLongClick: (tab: TabSessionState) -> Unit,
) {
val tabBorderModifier = if (isSelected && !multiSelectionEnabled) {
val tabBorderModifier = if (isSelected) {
Modifier.border(
4.dp,
FirefoxTheme.colors.borderAccent,
@ -94,95 +107,131 @@ fun TabGridItem(
Modifier
}
Box(
modifier = Modifier
.wrapContentHeight()
.wrapContentWidth(),
val dismissState = rememberDismissState(
confirmStateChange = { dismissValue ->
if (dismissValue == DismissValue.DismissedToEnd || dismissValue == DismissValue.DismissedToStart) {
onCloseClick(tab)
true
} else {
false
}
},
)
// Used to propagate the ripple effect to the whole tab
val interactionSource = remember { MutableInteractionSource() }
SwipeToDismiss(
state = dismissState,
enabled = !multiSelectionEnabled,
backgroundContent = {},
modifier = Modifier.zIndex(
if (dismissState.dismissDirection == null) {
0f
} else {
1f
},
),
) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(202.dp)
.padding(4.dp)
.then(tabBorderModifier)
.padding(4.dp)
.combinedClickable(
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
),
elevation = 0.dp,
shape = RoundedCornerShape(dimensionResource(id = R.dimen.tab_tray_grid_item_border_radius)),
border = BorderStroke(1.dp, FirefoxTheme.colors.borderPrimary),
) {
Column(
modifier = Modifier.background(FirefoxTheme.colors.layer2),
Box(modifier = Modifier.wrapContentSize()) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(202.dp)
.padding(4.dp)
.then(tabBorderModifier)
.padding(4.dp)
.combinedClickable(
interactionSource = interactionSource,
indication = rememberRipple(
color = when (isSystemInDarkTheme()) {
true -> PhotonColors.White
false -> PhotonColors.Black
},
),
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
),
elevation = 0.dp,
shape = RoundedCornerShape(dimensionResource(id = R.dimen.tab_tray_grid_item_border_radius)),
border = BorderStroke(1.dp, FirefoxTheme.colors.borderPrimary),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
Column(
modifier = Modifier.background(FirefoxTheme.colors.layer2),
) {
Favicon(
url = tab.content.url,
size = 16.dp,
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp),
)
HorizontalFadingEdgeBox(
Row(
modifier = Modifier
.weight(1f)
.wrapContentHeight()
.requiredHeight(30.dp)
.padding(7.dp, 5.dp)
.clipToBounds(),
backgroundColor = FirefoxTheme.colors.layer2,
isContentRtl = BidiFormatter.getInstance().isRtl(tab.content.title),
.fillMaxWidth()
.wrapContentHeight(),
) {
Text(
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
fontSize = 14.sp,
maxLines = 1,
softWrap = false,
style = TextStyle(
color = FirefoxTheme.colors.textPrimary,
textDirection = TextDirection.Content,
),
)
}
Spacer(modifier = Modifier.width(8.dp))
if (!multiSelectionEnabled) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
tab.content.icon?.let { icon ->
icon.prepareToDraw()
Image(
bitmap = icon.asImageBitmap(),
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterVertically)
.size(16.dp),
)
}
HorizontalFadingEdgeBox(
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically)
.testTag(TabsTrayTestTag.tabItemClose),
)
.weight(1f)
.wrapContentHeight()
.requiredHeight(30.dp)
.padding(7.dp, 5.dp)
.clipToBounds(),
backgroundColor = FirefoxTheme.colors.layer2,
isContentRtl = BidiFormatter.getInstance().isRtl(tab.content.title),
) {
Text(
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
fontSize = 14.sp,
maxLines = 1,
softWrap = false,
style = TextStyle(
color = FirefoxTheme.colors.textPrimary,
textDirection = TextDirection.Content,
),
)
}
if (!multiSelectionEnabled) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(id = R.string.close_tab),
tint = FirefoxTheme.colors.iconPrimary,
modifier = Modifier
.clickable { onCloseClick(tab) }
.size(24.dp)
.align(Alignment.CenterVertically)
.testTag(TabsTrayTestTag.tabItemClose),
)
}
}
}
Divider()
Divider()
Thumbnail(
tab = tab,
multiSelectionSelected = multiSelectionSelected,
)
}
}
Thumbnail(
if (!multiSelectionEnabled) {
MediaImage(
tab = tab,
multiSelectionSelected = multiSelectionSelected,
onMediaIconClicked = { onMediaClick(tab) },
modifier = Modifier
.align(Alignment.TopStart),
interactionSource = interactionSource,
)
}
}
if (!multiSelectionEnabled) {
MediaImage(
tab = tab,
onMediaIconClicked = { onMediaClick(tab) },
modifier = Modifier
.align(Alignment.TopStart),
)
}
}
}
@ -205,9 +254,8 @@ private fun Thumbnail(
testTag = TabsTrayTestTag.tabItemThumbnail
},
) {
ThumbnailCard(
url = tab.content.url,
key = tab.id,
TabThumbnail(
tab = tab,
size = LocalConfiguration.current.screenWidthDp.dp,
modifier = Modifier.fillMaxSize(),
)

@ -7,6 +7,8 @@ package org.mozilla.fenix.compose.tabstray
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@ -15,13 +17,20 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.DismissValue
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.rememberDismissState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -34,8 +43,10 @@ import androidx.compose.ui.unit.sp
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.support.ktx.kotlin.MAX_URI_LENGTH
import mozilla.components.ui.colors.PhotonColors
import org.mozilla.fenix.R
import org.mozilla.fenix.compose.ThumbnailCard
import org.mozilla.fenix.compose.SwipeToDismiss
import org.mozilla.fenix.compose.TabThumbnail
import org.mozilla.fenix.compose.annotation.LightDarkPreview
import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.tabstray.TabsTrayTestTag
@ -57,9 +68,9 @@ import org.mozilla.fenix.theme.FirefoxTheme
* @param onClick Callback to handle when item is clicked.
* @param onLongClick Callback to handle when item is long clicked.
*/
@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
@Suppress("MagicNumber")
@Suppress("MagicNumber", "LongMethod")
fun TabListItem(
tab: TabSessionState,
isSelected: Boolean = false,
@ -75,66 +86,98 @@ fun TabListItem(
} else {
FirefoxTheme.colors.layer1
}
Row(
modifier = Modifier
.fillMaxWidth()
.background(contentBackgroundColor)
.combinedClickable(
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
)
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Thumbnail(
tab = tab,
multiSelectionEnabled = multiSelectionEnabled,
isSelected = multiSelectionSelected,
onMediaIconClicked = { onMediaClick(it) },
)
Column(
val dismissState = rememberDismissState(
confirmStateChange = { dismissValue ->
if (dismissValue == DismissValue.DismissedToEnd || dismissValue == DismissValue.DismissedToStart) {
onCloseClick(tab)
true
} else {
false
}
},
)
// Used to propagate the ripple effect to the whole tab
val interactionSource = remember { MutableInteractionSource() }
SwipeToDismiss(
state = dismissState,
enabled = !multiSelectionEnabled,
backgroundContent = {
DismissedTabBackground(dismissState.dismissDirection, RoundedCornerShape(0.dp))
},
) {
Row(
modifier = Modifier
.padding(start = 12.dp)
.weight(weight = 1f),
.fillMaxWidth()
.background(FirefoxTheme.colors.layer3)
.background(contentBackgroundColor)
.combinedClickable(
interactionSource = interactionSource,
indication = rememberRipple(
color = when (isSystemInDarkTheme()) {
true -> PhotonColors.White
false -> PhotonColors.Black
},
),
onLongClick = { onLongClick(tab) },
onClick = { onClick(tab) },
)
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
Text(
text = tab.content.url.toShortUrl(),
color = FirefoxTheme.colors.textSecondary,
fontSize = 14.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
Thumbnail(
tab = tab,
multiSelectionEnabled = multiSelectionEnabled,
isSelected = multiSelectionSelected,
onMediaIconClicked = { onMediaClick(it) },
interactionSource = interactionSource,
)
}
if (!multiSelectionEnabled) {
IconButton(
onClick = { onCloseClick(tab) },
Column(
modifier = Modifier
.size(size = 48.dp)
.testTag(TabsTrayTestTag.tabItemClose),
.padding(start = 12.dp)
.weight(weight = 1f),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(
id = R.string.close_tab_title,
tab.content.title,
),
tint = FirefoxTheme.colors.iconPrimary,
Text(
text = tab.toDisplayTitle().take(MAX_URI_LENGTH),
color = FirefoxTheme.colors.textPrimary,
fontSize = 16.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 2,
)
Text(
text = tab.content.url.toShortUrl(),
color = FirefoxTheme.colors.textSecondary,
fontSize = 14.sp,
letterSpacing = 0.0.sp,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
if (!multiSelectionEnabled) {
IconButton(
onClick = { onCloseClick(tab) },
modifier = Modifier
.size(size = 48.dp)
.testTag(TabsTrayTestTag.tabItemClose),
) {
Icon(
painter = painterResource(id = R.drawable.mozac_ic_close),
contentDescription = stringResource(
id = R.string.close_tab_title,
tab.content.title,
),
tint = FirefoxTheme.colors.iconPrimary,
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
}
}
}
}
@ -145,11 +188,11 @@ private fun Thumbnail(
multiSelectionEnabled: Boolean,
isSelected: Boolean,
onMediaIconClicked: ((TabSessionState) -> Unit),
interactionSource: MutableInteractionSource,
) {
Box {
ThumbnailCard(
url = tab.content.url,
key = tab.id,
TabThumbnail(
tab = tab,
modifier = Modifier
.size(width = 92.dp, height = 72.dp)
.semantics(mergeDescendants = true) {
@ -159,6 +202,13 @@ private fun Thumbnail(
)
if (isSelected) {
Box(
modifier = Modifier
.size(width = 92.dp, height = 72.dp)
.clip(RoundedCornerShape(4.dp))
.background(FirefoxTheme.colors.layerAccentNonOpaque),
)
Card(
modifier = Modifier
.size(size = 40.dp)
@ -182,6 +232,7 @@ private fun Thumbnail(
tab = tab,
onMediaIconClicked = onMediaIconClicked,
modifier = Modifier.align(Alignment.TopEnd),
interactionSource = interactionSource,
)
}
}

@ -7,7 +7,7 @@ package org.mozilla.fenix.crashes
import android.view.ViewGroup.MarginLayoutParams
import androidx.navigation.NavController
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChangedBy
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.normalTabs
@ -17,7 +17,6 @@ import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.lib.state.helpers.AbstractBinding
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.utils.Settings
@ -53,7 +52,7 @@ class CrashContentIntegration(
) : AbstractBinding<BrowserState>(browserStore) {
override suspend fun onState(flow: Flow<BrowserState>) {
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(sessionId) }
.ifChanged { tab -> tab.engineState.crashed }
.distinctUntilChangedBy { tab -> tab.engineState.crashed }
.collect { tab ->
if (tab.engineState.crashed) {
toolbar.expand()

@ -0,0 +1,57 @@
/* 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.crashes
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.base.crash.CrashReporting
import mozilla.components.feature.contextmenu.facts.ContextMenuFacts
import mozilla.components.feature.downloads.facts.DownloadsFacts
import mozilla.components.feature.prompts.facts.AddressAutofillDialogFacts
import mozilla.components.feature.prompts.facts.CreditCardAutofillDialogFacts
import mozilla.components.feature.prompts.facts.PromptFacts
import mozilla.components.feature.sitepermissions.SitePermissionsFacts
import mozilla.components.support.base.Component
import mozilla.components.support.base.facts.Fact
import mozilla.components.support.base.facts.FactProcessor
import mozilla.components.support.base.facts.Facts
/**
* Collects facts and record bread crumbs for the events.
*/
class CrashFactCollector(
private val crashReporter: CrashReporting,
) {
/**
* Starts collecting facts.
*/
fun start() {
Facts.registerProcessor(
object : FactProcessor {
override fun process(fact: Fact) {
fact.process()
}
},
)
}
internal fun Fact.process(): Unit = when (component to item) {
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.MENU,
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_SHOWN,
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_SAVE_PROMPT_SHOWN,
Component.FEATURE_DOWNLOADS to CreditCardAutofillDialogFacts.Items.AUTOFILL_CREDIT_CARD_PROMPT_DISMISSED,
Component.FEATURE_DOWNLOADS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_SHOWN,
Component.FEATURE_DOWNLOADS to AddressAutofillDialogFacts.Items.AUTOFILL_ADDRESS_PROMPT_DISMISSED,
Component.FEATURE_DOWNLOADS to DownloadsFacts.Items.PROMPT,
Component.FEATURE_SITEPERMISSIONS to SitePermissionsFacts.Items.PERMISSIONS,
Component.FEATURE_PROMPTS to PromptFacts.Items.PROMPT,
-> {
crashReporter.recordCrashBreadcrumb(Breadcrumb("$component $action $value"))
}
else -> {
// no-op
}
}
}

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

Loading…
Cancel
Save