diff --git a/.buildconfig.yml b/.buildconfig.yml index 62d820f7a..808a4246c 100644 --- a/.buildconfig.yml +++ b/.buildconfig.yml @@ -80,7 +80,6 @@ projects: - support-rusterrors - support-rusthttp - support-rustlog - - support-sync-telemetry - support-test - support-test-libstate - support-utils diff --git a/android-components b/android-components index ac015fe2d..51fe6a7e4 160000 --- a/android-components +++ b/android-components @@ -1 +1 @@ -Subproject commit ac015fe2d5ef0700f93e40b62094f1cf79edcf86 +Subproject commit 51fe6a7e4c1e4d01946aadddf21b788e8ce7fff7 diff --git a/app/.experimenter.yaml b/app/.experimenter.yaml index 0fe5593ef..aca712aaf 100644 --- a/app/.experimenter.yaml +++ b/app/.experimenter.yaml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 24d6ac02c..b2c042323 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 -> diff --git a/app/metrics.yaml b/app/metrics.yaml index b45bd2c92..831edbada 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -143,13 +143,14 @@ events: bookmarks, desktop_view_off, desktop_view_on, downloads, find_in_page, forward, history, new_tab, open_in_app, open_in_fenix, quit, reader_mode_appearance, reload, remove_from_top_sites, - save_to_collection, set_default_browser, settings, share, stop, and - sync_account. + save_to_collection, set_default_browser, settings, share, stop, + sync_account, and print_content. type: string bugs: - https://github.com/mozilla-mobile/fenix/issues/1024 - https://github.com/mozilla-mobile/fenix/issues/19923 - https://bugzilla.mozilla.org/show_bug.cgi?id=1808689 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836780 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708 - https://github.com/mozilla-mobile/fenix/pull/5098#issuecomment-529658996 @@ -159,6 +160,7 @@ events: - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/21316#issuecomment-944615938 - https://github.com/mozilla-mobile/fenix/pull/27295 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1837517#c3 data_sensitivity: - interaction notification_emails: @@ -476,19 +478,29 @@ events: - https://github.com/mozilla-mobile/fenix/pull/19959#issuecomment-882539619 - https://github.com/mozilla-mobile/fenix/pull/24409 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never save_to_pdf_tapped: type: event description: | A user tapped the save to pdf option in the share sheet. + extra_keys: + source: + type: string + description: | + A string that indicates the type of document of pdf, non-pdf or unknown. + The default is unknown. bugs: - https://github.com/mozilla-mobile/fenix/issues/3709 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/27257 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213#c4 data_sensitivity: - interaction notification_emails: @@ -500,12 +512,49 @@ events: save_to_pdf_failure: type: event description: | - A user tapped the save pdf but an error ocurred + A user tapped the save pdf but an error occurred and the process failed. + extra_keys: + source: + type: string + description: | + A string that indicates the type of document of pdf, non-pdf or unknown. + The default is unknown. + reason: + type: string + description: | + An error occurred while setting up for saving a PDF. + Default option is unknown, other options are no_settings_service, no_settings, + no_canonical_context, no_activity_context_delegate, no_activity_context, + no_print_delegate, and io_error. bugs: - https://github.com/mozilla-mobile/fenix/issues/27635 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/27661#issuecomment-1300505370 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213#c4 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: 122 + metadata: + tags: + - Sharing + save_to_pdf_completed: + type: event + description: | + Saving to PDF successfully generated a PDF. + extra_keys: + source: + type: string + description: | + A string that indicates the type of document of pdf, non-pdf or unknown. + The default is unknown. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1829213#c4 data_sensitivity: - technical notification_emails: @@ -588,6 +637,27 @@ events: tags: - Search +splash_screen: + first_launch_extended: + type: event + description: | + The splash screen was shown for an extended period of time, providing more time + to download marketing and experiment data. + extra_keys: + data_fetched: + type: boolean + description: | + If the splash screen was closed due to data being fetched or due to the time running out. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1840315 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2616 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: 126 + onboarding: syn_cfr_shown: type: event @@ -1607,6 +1677,103 @@ metrics: metadata: tags: - China + bookmarks_add: + type: labeled_counter + lifetime: application + description: | + A counter that indicates how many bookmarks a user has added. + + The label for this counter is ``. + + `source` will be: `page_action_menu` as that is the only + entry point right now to add bookmarks. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836167 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2375 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Bookmarks + bookmarks_edit: + type: labeled_counter + lifetime: application + description: | + A counter that indicates how many bookmarks a user has edited. + + The label for this counter is ``. + + `source` will be: `bookmark_edit_page` or `bookmark_panel`. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836167 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2375 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Bookmarks + bookmarks_delete: + type: labeled_counter + lifetime: application + description: | + A counter that indicates how many bookmarks a user has deleted. + + The label for this counter is ``. + + `source` will be: `add_bookmark_toast` or `bookmark_panel`. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836167 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2375 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Bookmarks + bookmarks_open: + type: labeled_counter + lifetime: application + description: | + A counter that indicates how many bookmarks a user has opened. + + The label for this counter is ``. + + `source` will be: `top_sites`, `awesomebar_results`, `bookmark_panel`. + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836167 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2375 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Bookmarks mobile_bookmarks_count: type: counter lifetime: application @@ -1625,7 +1792,7 @@ metrics: - https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701 - https://github.com/mozilla-mobile/fenix/pull/19924#issuecomment-861423789 - https://github.com/mozilla-mobile/fenix/pull/20517#pullrequestreview-718069041 - - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-906757301 + - https://github.com/mozilla-mobile/fenix/pull/21038#issuecomment-9067573./01 data_sensitivity: - interaction notification_emails: @@ -2096,6 +2263,30 @@ metrics: tags: - Discovery - Search + private_tabs_open_count: + type: counter + lifetime: application + description: | + A counter that indicates how many PRIVATE tabs a user has open. This + value will only be set if the user has at least *one* open tab. If they + have 0, this ping will not get sent, resulting in a null value. To + disambiguate between a failed `private_tabs_open_count` ping and 0 open tabs, + please see `has_open_tabs` + send_in_pings: + - metrics + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836165 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2427 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Tabs tabs_open_count: type: counter lifetime: application @@ -2266,26 +2457,24 @@ metrics: metadata: tags: - Notifications - shared_prefs_uuid: - type: uuid - lifetime: ping - description: | - A UUID stored in Shared Preferences used to analyze technical differences - between storage mechanisms in Android, specifically the Glean DB and - Shared Preferences. + ram_more_than_threshold: + type: boolean + lifetime: application + description: True if the device's asserted 'advertised' RAM is more than the given threshold. send_in_pings: - metrics - notification_emails: - - android-probes@mozilla.com - - raphael@mozilla.com - - fbertsch@mozilla.com bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1822119 + - https://bugzilla.mozilla.org/show_bug.cgi?id=1840341 data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1822119 + - https://github.com/mozilla-mobile/firefox-android/pull/2620 data_sensitivity: - technical - expires: 116 + notification_emails: + - android-probes@mozilla.com + expires: 128 + metadata: + tags: + - Experiments customize_home: most_visited_sites: @@ -2301,11 +2490,13 @@ customize_home: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never jump_back_in: type: boolean description: | @@ -2319,11 +2510,13 @@ customize_home: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never recently_saved: type: boolean description: | @@ -2337,11 +2530,13 @@ customize_home: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never recently_visited: type: boolean description: | @@ -2355,11 +2550,13 @@ customize_home: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never pocket: type: boolean description: | @@ -2372,11 +2569,13 @@ customize_home: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - PocketIntegration @@ -2392,11 +2591,13 @@ customize_home: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25418#issuecomment-1163390855 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - PocketIntegration @@ -2440,12 +2641,14 @@ customize_home: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1896 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - technical - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never opening_screen: type: string description: | @@ -3764,7 +3967,25 @@ sync_account: metadata: tags: - SendTab - +settings: + sign_into_sync: + type: counter + description: | + Counts the number of times a user has clicked "sign into sync" from the settings page. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836166 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2550 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Sync + - Settings history: opened: type: event @@ -4079,11 +4300,13 @@ history: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/23695 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never search_result_tapped: type: event description: | @@ -4093,11 +4316,13 @@ history: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/23695 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never recently_closed_tabs: opened: @@ -5906,6 +6131,75 @@ logins: metadata: tags: - Logins + saved: + type: counter + description: | + Counter of number of passwords that have been saved by user (including deleted). + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Logins + saved_all: + type: quantity + description: | + Counter of number of passwords currently saved by user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + unit: integer + metadata: + tags: + - Logins + deleted: + type: counter + description: | + Counter of number of passwords that have been deleted by user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Logins + modified: + type: counter + description: | + Counter of number of passwords that have been modified by user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Logins voice_search: tapped: @@ -6389,7 +6683,25 @@ top_sites: metadata: tags: - Shortcuts - +app_menu: + sign_into_sync: + type: counter + description: | + Counts the number of times a user has clicked "sign into sync" from the settings page. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836166 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2550 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Sync + - Settings app_theme: dark_theme_selected: type: event @@ -6841,6 +7153,28 @@ first_session: tags: - Performance - Attribution + adjust_attribution_timespan: + type: timespan + time_unit: millisecond + send_in_pings: + - first-session + - metrics + description: > + The time that it takes to derive the attribution parameters by + the Adjust SDK. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1823492 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2974 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: 124 + metadata: + tags: + - Performance + - Attribution play_store_attribution: source: type: string @@ -6958,6 +7292,27 @@ play_store_attribution: tags: - Attribution - Performance + deferred_deeplink_time: + type: timespan + time_unit: millisecond + send_in_pings: + - metrics + description: | + The time that it takes to receive deferred deeplink from the Google Play Store. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1843610 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2851 + data_sensitivity: + - technical + notification_emails: + - android-probes@mozilla.com + expires: 128 + metadata: + tags: + - Attribution + - Performance + browser.search: with_ads: type: labeled_counter @@ -7593,11 +7948,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1796146 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/27561 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7615,11 +7971,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1796146 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/27561 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7632,11 +7989,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7649,11 +8007,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7664,11 +8023,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797577 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28044#issuecomment-1334548056 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7679,11 +8039,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7696,11 +8057,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7713,11 +8075,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7730,11 +8093,12 @@ cookie_banners: - https://bugzilla.mozilla.org/show_bug.cgi?id=1797593 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/28405#issuecomment-1372489596 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - Privacy&Security @@ -7983,11 +8347,12 @@ progressive_web_app: - https://github.com/mozilla-mobile/fenix/pull/21076#issuecomment-909237275 - https://github.com/mozilla-mobile/fenix/pull/23783#issuecomment-1041863879 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - PWA @@ -8004,11 +8369,12 @@ progressive_web_app: - https://github.com/mozilla-mobile/fenix/pull/21076#issuecomment-909237275 - https://github.com/mozilla-mobile/fenix/pull/23783#issuecomment-1041863879 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 metadata: tags: - PWA @@ -8528,6 +8894,24 @@ home_screen: metadata: tags: - HomeScreen + standard_homepage_view_count: + type: counter + description: | + The number of times the standard browsing mode home screen was + displayed to the user. (for tile counts) + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1842082 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2841 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - HomeScreen home_screen_view_count: type: counter description: | @@ -8550,11 +8934,13 @@ home_screen: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/21344 - https://github.com/mozilla-mobile/fenix/pull/21344#issuecomment-923198787 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never start_on_home: enter_home_screen: @@ -8776,11 +9162,12 @@ recent_searches: - https://github.com/mozilla-mobile/fenix/pull/22176#issuecomment-956421788 - https://github.com/mozilla-mobile/fenix/pull/23786#issuecomment-1042331298 - https://github.com/mozilla-mobile/fenix/pull/28709#issuecomment-1410276888 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + expires: 123 credit_cards: saved: @@ -8793,11 +9180,31 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Autofill + saved_all: + type: quantity + description: | + Counter of number of credit cards that are currently stored by user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never + unit: integer metadata: tags: - Autofill @@ -8811,11 +9218,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8828,11 +9237,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8845,11 +9256,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8862,11 +9275,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8879,11 +9294,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8896,11 +9313,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8913,11 +9332,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8930,11 +9351,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8947,11 +9370,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8964,11 +9389,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25411 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8981,11 +9408,13 @@ credit_cards: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25411 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -8997,11 +9426,13 @@ credit_cards: - https://github.com/mozilla-mobile/fenix/issues/26089 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/26095 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9017,11 +9448,31 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 + data_sensitivity: + - interaction + notification_emails: + - android-probes@mozilla.com + - cgordon@mozilla.com + expires: never + metadata: + tags: + - Autofill + saved_all: + type: quantity + description: | + A counter of the number of all addresses that are currently saved by user. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1836164 + data_reviews: + - https://github.com/mozilla-mobile/firefox-android/pull/2555 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never + unit: integer metadata: tags: - Autofill @@ -9035,11 +9486,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9053,11 +9506,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9070,11 +9525,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9087,11 +9544,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9104,11 +9563,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9121,11 +9582,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9138,11 +9601,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9155,11 +9620,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/20909 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9172,11 +9639,13 @@ addresses: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/25216 - https://github.com/mozilla-mobile/fenix/pull/26123#issuecomment-1190794469 + - https://github.com/mozilla-mobile/firefox-android/pull/2597 data_sensitivity: - interaction notification_emails: - android-probes@mozilla.com - expires: 118 + - cgordon@mozilla.com + expires: never metadata: tags: - Autofill @@ -9473,23 +9942,6 @@ private_browsing_shortcut_cfr: - android-probes@mozilla.com expires: 122 -server_knobs: - validation: - disabled: true - type: event - description: | - Temporary metric recorded at the same time as - "tabs_tray.new_tab_tapped" to validate that the Glean Server Knobs - functionality is working correctly. - bugs: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1823682 - data_reviews: - - https://bugzilla.mozilla.org/show_bug.cgi?id=1823682#c2 - notification_emails: - - android-probes@mozilla.com - - brosa@mozilla.com - expires: 116 - pull_to_refresh_in_browser: enabled: type: boolean diff --git a/app/nimbus.fml.yaml b/app/nimbus.fml.yaml index 271d32342..a982836f2 100644 --- a/app/nimbus.fml.yaml +++ b/app/nimbus.fml.yaml @@ -320,6 +320,41 @@ features: type: Map 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 + default: {} types: objects: {} diff --git a/app/src/androidTest/assets/pages/generic3.html b/app/src/androidTest/assets/pages/generic3.html index 213954c95..2f033ec8d 100644 --- a/app/src/androidTest/assets/pages/generic3.html +++ b/app/src/androidTest/assets/pages/generic3.html @@ -9,7 +9,7 @@ Mozilla Playstore link

- PDF file + PDF form file

diff --git a/app/src/androidTest/assets/resources/washington.pdf b/app/src/androidTest/assets/resources/pdfForm.pdf similarity index 95% rename from app/src/androidTest/assets/resources/washington.pdf rename to app/src/androidTest/assets/resources/pdfForm.pdf index 19ecef099..8c4768249 100644 Binary files a/app/src/androidTest/assets/resources/washington.pdf and b/app/src/androidTest/assets/resources/pdfForm.pdf differ diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt index eb0888984..a7cf3e315 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/Constants.kt @@ -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" diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt index a95fb9216..d5c6cc538 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelper.kt @@ -82,6 +82,11 @@ interface FeatureSettingsHelper { */ var tabsTrayRewriteEnabled: Boolean + /** + * Enable or disable the Unified search feature. + */ + var isUnifiedSearchEnabled: Boolean + fun applyFlagUpdates() fun resetAllFeatureFlags() diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt index 8ccd170d4..eefa11bb5 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/FeatureSettingsHelperDelegate.kt @@ -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, diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt index 519ffeff5..9f58f3da2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt @@ -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, diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 0bb18371b..aaa463850 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -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()) } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt index c970b9e02..9b0717d75 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ComposeTabbedBrowsingTest.kt @@ -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") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt index 88be02a09..e3f19dee0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -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() + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt index 9c547cf00..49de05b1b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/CookieBannerReductionTest.kt @@ -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) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt index 0b66ae82c..5b3c2de7c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -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) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt index 0ffb4e226..dcf50b85b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -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()) } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt index e1684eee7..96bf01118 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HomeScreenTest.kt @@ -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") } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt index fd5a97789..e28ae05f6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/LoginsTest.kt @@ -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" diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt index 9880fab8b..a0ded6e11 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/MediaNotificationTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index bd4a77a1d..377d3213d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -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") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt new file mode 100644 index 000000000..9b50e27f3 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/PDFViewerTest.kt @@ -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) + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt index 547b7d088..41cb35f48 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/PwaTest.kt @@ -56,6 +56,7 @@ class PwaTest { } } + @Ignore("Failing, see https://bugzilla.mozilla.org/show_bug.cgi?id=1807275") @SmokeTest @Test fun emailLinkPWATest() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt new file mode 100644 index 000000000..7e306e1f8 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/RecentlyClosedTabsTest.kt @@ -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() + } + } +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt index 14b7f1fc6..b2c6a93d1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt @@ -87,7 +87,7 @@ class SearchTest { homeScreen { }.openSearch { verifySearchView() - verifyBrowserToolbar() + verifySearchToolbar(true) verifyScanButton() verifySearchEngineButton() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt index 417d54bd3..8f3236701 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt index fdaac09ba..597a41c9b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAdvancedTest.kt @@ -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() } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt index 517a3507f..e528e0f2f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataOnQuitTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt index e97336d36..92eb2693d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeleteBrowsingDataTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt index 94341b90c..e62ec5832 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsDeveloperToolsTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt index a9e1a4ab5..36b5e32af 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsGeneralTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt index a5f334bd7..e182fd593 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt @@ -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") + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt index bc479ea60..86777960c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsSyncTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt index 7568dadb9..83376ad65 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -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() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt index a4d097fce..2e486874f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -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() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt index e253a90c9..7f53a9205 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TextSelectionTest.kt @@ -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()) + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt index b111ea4e2..afa02ef05 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt @@ -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 diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md b/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md new file mode 100644 index 000000000..178e76df3 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/docs/channels.md @@ -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. diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt index 13fda0b02..9d3e78074 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt @@ -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)), ), ) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt index 9d477d97c..15a22edca 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -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() + } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt index 1e9924568..5931e898a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ComposeTabDrawerRobot.kt @@ -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 { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt index 2560502a1..6bca13907 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt @@ -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() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt index 46afbb1c6..2553b5395 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HistoryRobot.kt @@ -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")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt index 06c4d07b6..c1f199e1e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt @@ -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() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt index 4a550debd..6c3d26053 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt @@ -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))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt index 5466ec1bb..01f89b093 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt @@ -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() + } } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt index 4e492a352..318f2dc89 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt @@ -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)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt index c4b5f7ca2..9b2869024 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt @@ -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), diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index 1e8790e5a..bccc3c225 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -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) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt index a6e549650..26655f2da 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAccessibilityRobot.kt @@ -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") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt index 8c2292d62..0c0cf2ebc 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAddonsManagerRobot.kt @@ -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()) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt index 22520a3cd..7df5a8994 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SystemSettingsRobot.kt @@ -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") diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt index 2fe3224e0..0412ff9b5 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt @@ -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()) - 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() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt index b273c8aa5..3ab1f69fe 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt @@ -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)) diff --git a/app/src/beta/res/drawable/animated_splash_screen.xml b/app/src/beta/res/drawable/animated_splash_screen.xml new file mode 100644 index 000000000..5d0d00b8e --- /dev/null +++ b/app/src/beta/res/drawable/animated_splash_screen.xml @@ -0,0 +1,563 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/debug/res/drawable/animated_splash_screen.xml b/app/src/debug/res/drawable/animated_splash_screen.xml new file mode 100644 index 000000000..4e96325db --- /dev/null +++ b/app/src/debug/res/drawable/animated_splash_screen.xml @@ -0,0 +1,1033 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a9d41ec47..302b5ed10 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,6 +99,7 @@ + diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 13038cbda..151c837bf 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 2fde59ba9..efa98ed8a 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 5d1724eeb..ad03da8ea 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt index ccee01685..168ab2fe9 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonPopupBaseFragment.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index e574311f7..7d7c8dc80 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -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() + private var addons: List = 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 = 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" } } diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt index 593beb49a..a58fa9596 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt index d3841a7ba..06dc61344 100644 --- a/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/android/FenixDialogFragment.kt @@ -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 { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index e77a68d33..f8ebc8f9b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -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() private val swipeRefreshFeature = ViewBoundFeatureWrapper() private val webchannelIntegration = ViewBoundFeatureWrapper() + private val webExtensionPromptFeature = ViewBoundFeatureWrapper() private val sitePermissionWifiIntegration = ViewBoundFeatureWrapper() private val secureWindowFeature = ViewBoundFeatureWrapper() @@ -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 = listOf( SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.PRIVATE_NOTICE), @@ -1605,4 +1642,13 @@ abstract class BaseBrowserFragment : return isValidStatus && isSameTab } + + private suspend fun provideAddons(): List { + 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 + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 2e0f5111d..e3fd70d8a 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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() private val openInAppOnboardingObserver = ViewBoundFeatureWrapper() + private val reviewQualityCheckFeature = ViewBoundFeatureWrapper() 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) diff --git a/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt index a9ad9b7ec..a661d1ea1 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/SwipeGestureLayout.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt b/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt index c64b8fc67..a4736069d 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt index cd98d98de..fa907471f 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt @@ -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 } }, ) diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt index 683d45862..6b859c769 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt index 29a3bc76a..01d303d44 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index b620c393f..f44d92868 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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(), diff --git a/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt b/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt index b6914e1a3..8d6dd4a6a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt index cbc70d0e8..cbd197d5c 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt @@ -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) } /** diff --git a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt index 4263d81b5..3ac7a7d8d 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt @@ -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 { diff --git a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt index 00cf6002b..69ad92448 100644 --- a/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/components/history/PagedHistoryProvider.kt @@ -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. diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt index 453f6a1bb..7fca99373 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AdjustMetricsService.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/FirstSessionPing.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/FirstSessionPing.kt index 7d1da9e35..9474ff079 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/FirstSessionPing.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/FirstSessionPing.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt index 21a64fc45..1c27729e6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt @@ -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. diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt index fc42937b7..34c71ac9a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MozillaProductDetector.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt index 101686648..f343c7cd8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -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 -> diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt index 421e30600..21a798069 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/DefaultToolbarMenu.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index 885e9e0d9..a4f41ccdb 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/compose/BottomSheetHandle.kt b/app/src/main/java/org/mozilla/fenix/compose/BottomSheetHandle.kt new file mode 100644 index 000000000..8e509354e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/BottomSheetHandle.kt @@ -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), + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt b/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt index 410dd9cdc..9a0429702 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ClickableSubstringLink.kt @@ -83,8 +83,8 @@ fun ClickableSubstringLink( annotatedText .getStringAnnotations("link", it, it) .firstOrNull()?.let { - onClick() - } + onClick() + } }, ) } diff --git a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt index 9e6c562d4..889372625 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLarge.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt index 31055da79..2e867c9da 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ListItemTabLargePlaceholder.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/compose/PagerIndicator.kt b/app/src/main/java/org/mozilla/fenix/compose/PagerIndicator.kt index addbaa703..301e8cb25 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/PagerIndicator.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/PagerIndicator.kt @@ -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 = diff --git a/app/src/main/java/org/mozilla/fenix/compose/SwipeToDismiss.kt b/app/src/main/java/org/mozilla/fenix/compose/SwipeToDismiss.kt new file mode 100644 index 000000000..3668cbf64 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/SwipeToDismiss.kt @@ -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 = 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, 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), + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/TabThumbnail.kt b/app/src/main/java/org/mozilla/fenix/compose/TabThumbnail.kt new file mode 100644 index 000000000..36e58d52d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/TabThumbnail.kt @@ -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)), + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt index 7eb0b91d9..2f6b3cc13 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailCard.kt @@ -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(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)), ) } diff --git a/app/src/main/java/org/mozilla/fenix/compose/ThumbnailImage.kt b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailImage.kt new file mode 100644 index 000000000..a144913d3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/ThumbnailImage.kt @@ -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 = {}, + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt b/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt index af357f7ea..c48722423 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/list/ListItem.kt @@ -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 = "", + ) } } } diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt new file mode 100644 index 000000000..802457d4f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/DismissedTabBackground.kt @@ -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), + ) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt index 7ce0d3844..e6c37dcb1 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/MediaImage.kt @@ -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) }, ) } diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabGridItem.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabGridItem.kt index 126ad4db6..0ede9ca9a 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabGridItem.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabGridItem.kt @@ -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(), ) diff --git a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt index ec67b0199..7340e74fd 100644 --- a/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt +++ b/app/src/main/java/org/mozilla/fenix/compose/tabstray/TabListItem.kt @@ -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, ) } } diff --git a/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt b/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt index 05f6a69c8..c866a0476 100644 --- a/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/crashes/CrashContentIntegration.kt @@ -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(browserStore) { override suspend fun onState(flow: Flow) { flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(sessionId) } - .ifChanged { tab -> tab.engineState.crashed } + .distinctUntilChangedBy { tab -> tab.engineState.crashed } .collect { tab -> if (tab.engineState.crashed) { toolbar.expand() diff --git a/app/src/main/java/org/mozilla/fenix/crashes/CrashFactCollector.kt b/app/src/main/java/org/mozilla/fenix/crashes/CrashFactCollector.kt new file mode 100644 index 000000000..ae0070a46 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/crashes/CrashFactCollector.kt @@ -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 + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index b2cf2747f..3b4e88833 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -23,6 +23,7 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.customtabs.CustomTabWindowFeature import mozilla.components.feature.pwa.feature.ManifestUpdateFeature import mozilla.components.feature.pwa.feature.WebAppActivityFeature +import mozilla.components.feature.pwa.feature.WebAppContentFeature import mozilla.components.feature.pwa.feature.WebAppHideToolbarFeature import mozilla.components.feature.pwa.feature.WebAppSiteControlsFeature import mozilla.components.support.base.feature.UserInteractionHandler @@ -126,6 +127,11 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler components.core.icons, manifest, ), + WebAppContentFeature( + store = requireComponents.core.store, + tabId = customTabSessionId, + manifest, + ), ManifestUpdateFeature( activity.applicationContext, requireComponents.core.store, diff --git a/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt b/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt index c2eeec492..6ed2c57fb 100644 --- a/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt +++ b/app/src/main/java/org/mozilla/fenix/downloads/StartDownloadDialog.kt @@ -20,6 +20,7 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.view.ViewCompat import androidx.core.view.children import androidx.viewbinding.ViewBinding +import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.feature.downloads.databinding.MozacDownloaderChooserPromptBinding import mozilla.components.feature.downloads.toMegabyteOrKilobyteString import mozilla.components.feature.downloads.ui.DownloaderApp @@ -27,6 +28,7 @@ import mozilla.components.feature.downloads.ui.DownloaderAppAdapter import org.mozilla.fenix.R import org.mozilla.fenix.databinding.DialogScrimBinding import org.mozilla.fenix.databinding.StartDownloadDialogLayoutBinding +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings /** @@ -54,6 +56,9 @@ abstract class StartDownloadDialog( * @param container The [ViewGroup] in which the download view will be inflated. */ fun show(container: ViewGroup): StartDownloadDialog { + activity.components.analytics.crashReporter.recordCrashBreadcrumb( + Breadcrumb("StartDownloadDialog show"), + ) this.container = container val dialogParent = container.parent as? ViewGroup @@ -89,6 +94,9 @@ abstract class StartDownloadDialog( * @param callback The callback for when the view is dismissed. */ fun onDismiss(callback: () -> Unit): StartDownloadDialog { + activity.components.analytics.crashReporter.recordCrashBreadcrumb( + Breadcrumb("StartDownloadDialog onDismiss"), + ) this.onDismiss = callback return this } diff --git a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt index db7d1c159..d7b24c15e 100644 --- a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt +++ b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt @@ -82,6 +82,9 @@ fun createNimbus(context: Context, urlString: String?): NimbusApi { onApplyCallback = { FxNimbus.invalidateCachedValues() } + onFetchedCallback = { + context.settings().nimbusExperimentsFetched = true + } }.build(appInfo) } diff --git a/app/src/main/java/org/mozilla/fenix/ext/AddonManager.kt b/app/src/main/java/org/mozilla/fenix/ext/AddonManager.kt new file mode 100644 index 000000000..e45e55165 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/AddonManager.kt @@ -0,0 +1,26 @@ +/* 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.ext + +import mozilla.components.feature.addons.Addon +import mozilla.components.feature.addons.AddonManager +import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.Config + +/** + * Returns the list of all installed and recommended add-ons filters out all the + * add-ons on [BuildConfig.MOZILLA_ONLINE_ADDON_EXCLUSIONS]. + */ +suspend fun AddonManager.getFenixAddons(allowCache: Boolean = true): List { + val addons = getAddons(allowCache = allowCache) + val excludedAddonIDs = if (Config.channel.isMozillaOnline && + !BuildConfig.MOZILLA_ONLINE_ADDON_EXCLUSIONS.isNullOrEmpty() + ) { + BuildConfig.MOZILLA_ONLINE_ADDON_EXCLUSIONS.toList() + } else { + emptyList() + } + return addons.filterNot { it.id in excludedAddonIDs } +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt b/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt index 1d6f5388f..4fba1d942 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt @@ -6,15 +6,6 @@ package org.mozilla.fenix.ext import android.content.Context import mozilla.components.browser.storage.sync.PlacesBookmarksStorage -import mozilla.components.concept.storage.BookmarkNode val Context.bookmarkStorage: PlacesBookmarksStorage get() = components.core.bookmarksStorage - -/** - * Removes [children] from [BookmarkNode.children] and returns the new modified [BookmarkNode]. - */ -operator fun BookmarkNode.minus(children: Set): BookmarkNode { - val removedChildrenGuids = children.map { it.guid } - return this.copy(children = this.children?.filterNot { removedChildrenGuids.contains(it.guid) }) -} diff --git a/app/src/main/java/org/mozilla/fenix/ext/NotificationManagerCompat.kt b/app/src/main/java/org/mozilla/fenix/ext/NotificationManagerCompat.kt deleted file mode 100644 index 779126d4e..000000000 --- a/app/src/main/java/org/mozilla/fenix/ext/NotificationManagerCompat.kt +++ /dev/null @@ -1,60 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.ext - -import android.os.Build -import androidx.core.app.NotificationChannelCompat -import androidx.core.app.NotificationManagerCompat - -/** - * Returns whether notifications are enabled, catches any exception that was thrown from - * [NotificationManagerCompat.areNotificationsEnabled] and returns false. - */ -@Suppress("TooGenericExceptionCaught") -fun NotificationManagerCompat.areNotificationsEnabledSafe(): Boolean { - return try { - areNotificationsEnabled() - } catch (e: Exception) { - false - } -} - -/** - * If the channel does not exist or is null, this returns false. - * If the channel exists with importance more than [NotificationManagerCompat.IMPORTANCE_NONE] and - * notifications are enabled for the app, this returns true. - * On <= SDK 26, this checks if notifications are enabled for the app. - * - * @param channelId the id of the notification channel to check. - * @return true if the channel is enabled, false otherwise. - */ -fun NotificationManagerCompat.isNotificationChannelEnabled(channelId: String): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = getNotificationChannelSafe(channelId) - if (channel == null) { - false - } else { - areNotificationsEnabledSafe() && channel.importance != NotificationManagerCompat.IMPORTANCE_NONE - } - } else { - areNotificationsEnabledSafe() - } -} - -/** - * Returns the notification channel with the given [channelId], or null if the channel does not - * exist, catches any exception that was thrown by - * [NotificationManagerCompat.getNotificationChannelCompat] and returns null. - * - * @param channelId the id of the notification channel to check. - */ -@Suppress("TooGenericExceptionCaught") -private fun NotificationManagerCompat.getNotificationChannelSafe(channelId: String): NotificationChannelCompat? { - return try { - getNotificationChannelCompat(channelId) - } catch (e: Exception) { - null - } -} diff --git a/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt b/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt new file mode 100644 index 000000000..586081fa3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/extension/WebExtensionPromptFeature.kt @@ -0,0 +1,295 @@ +/* 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.extension + +import android.content.Context +import android.view.Gravity +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.mapNotNull +import mozilla.components.browser.state.action.WebExtensionAction +import mozilla.components.browser.state.state.extension.WebExtensionPromptRequest +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.addons.Addon +import mozilla.components.feature.addons.toInstalledState +import mozilla.components.feature.addons.ui.AddonInstallationDialogFragment +import mozilla.components.feature.addons.ui.PermissionsDialogFragment +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import org.mozilla.fenix.R +import org.mozilla.fenix.addons.showSnackBar +import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.theme.ThemeManager +import java.lang.ref.WeakReference + +/** + * Feature implementation for handling [WebExtensionPromptRequest] and showing the respective UI. + */ +class WebExtensionPromptFeature( + private val store: BrowserStore, + private val provideAddons: suspend () -> List, + private val context: Context, + private val view: View, + private val fragmentManager: FragmentManager, + private val onAddonChanged: (Addon) -> Unit = {}, +) : LifecycleAwareFeature { + + /** + * Whether or not an add-on installation is in progress. + */ + private var isInstallationInProgress = false + private var scope: CoroutineScope? = null + + /** + * Starts observing the selected session to listen for window requests + * and opens / closes tabs as needed. + */ + override fun start() { + scope = store.flowScoped { flow -> + flow.mapNotNull { state -> + state.webExtensionPromptRequest + }.distinctUntilChanged().collect { promptRequest -> + val addon = provideAddons().find { addon -> + addon.id == promptRequest.extension.id + } + when (promptRequest) { + is WebExtensionPromptRequest.Permissions -> handlePermissionRequest( + addon, + promptRequest, + ) + + is WebExtensionPromptRequest.PostInstallation -> handlePostInstallationRequest( + addon?.copy(installedState = promptRequest.extension.toInstalledState()), + ) + } + } + } + tryToReAttachButtonHandlersToPreviousDialog() + } + + private fun handlePostInstallationRequest( + addon: Addon?, + ) { + if (addon == null) { + consumePromptRequest() + return + } + showPostInstallationDialog(addon) + } + + private fun handlePermissionRequest( + addon: Addon?, + promptRequest: WebExtensionPromptRequest.Permissions, + ) { + if (hasExistingPermissionDialogFragment()) return + + // If the add-on is not found, it is already installed because the install process can only + // be triggered for add-ons "known" by Fenix (the add-on is either part of the official list + // of supported extensions OR part of the user custom AMO collection). + if (addon == null) { + promptRequest.onConfirm(false) + consumePromptRequest() + showSnackBar( + view, + context.getString(R.string.addon_already_installed), + FenixSnackbar.LENGTH_LONG, + ) + } else { + showPermissionDialog( + addon, + promptRequest, + ) + } + } + + /** + * Stops observing the selected session for incoming window requests. + */ + override fun stop() { + scope?.cancel() + } + + @VisibleForTesting + internal fun showPermissionDialog( + addon: Addon, + promptRequest: WebExtensionPromptRequest.Permissions, + ) { + if (!isInstallationInProgress && !hasExistingPermissionDialogFragment()) { + val dialog = PermissionsDialogFragment.newInstance( + addon = addon, + promptsStyling = PermissionsDialogFragment.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context, + ), + positiveButtonTextColor = ThemeManager.resolveAttribute( + R.attr.textOnColorPrimary, + context, + ), + positiveButtonRadius = + (context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), + ), + onPositiveButtonClicked = { + handleApprovedPermissions(promptRequest) + }, + onNegativeButtonClicked = { + handleDeniedPermissions(promptRequest) + }, + ) + dialog.show( + fragmentManager, + PERMISSIONS_DIALOG_FRAGMENT_TAG, + ) + } + } + + private fun tryToReAttachButtonHandlersToPreviousDialog() { + findPreviousPermissionDialogFragment()?.let { dialog -> + dialog.onPositiveButtonClicked = { addon -> + store.state.webExtensionPromptRequest?.let { promptRequest -> + if (addon.id == promptRequest.extension.id && + promptRequest is WebExtensionPromptRequest.Permissions + ) { + handleApprovedPermissions(promptRequest) + } + } + } + dialog.onNegativeButtonClicked = { + store.state.webExtensionPromptRequest?.let { promptRequest -> + if (promptRequest is WebExtensionPromptRequest.Permissions) { + handleDeniedPermissions(promptRequest) + } + } + } + } + + findPreviousPostInstallationDialogFragment()?.let { dialog -> + dialog.onConfirmButtonClicked = { addon, allowInPrivateBrowsing -> + store.state.webExtensionPromptRequest?.let { promptRequest -> + if (addon.id == promptRequest.extension.id && + promptRequest is WebExtensionPromptRequest.PostInstallation + ) { + handlePostInstallationButtonClicked( + allowInPrivateBrowsing = allowInPrivateBrowsing, + context = WeakReference(context), + addon = addon, + ) + } + } + } + dialog.onDismissed = { + store.state.webExtensionPromptRequest?.let { _ -> + consumePromptRequest() + } + } + } + } + + private fun handleDeniedPermissions(promptRequest: WebExtensionPromptRequest.Permissions) { + promptRequest.onConfirm(false) + consumePromptRequest() + } + + private fun handleApprovedPermissions(promptRequest: WebExtensionPromptRequest.Permissions) { + promptRequest.onConfirm(true) + consumePromptRequest() + } + + private fun consumePromptRequest() { + store.dispatch(WebExtensionAction.ConsumePromptRequestWebExtensionAction) + } + + private fun hasExistingPermissionDialogFragment(): Boolean { + return findPreviousPermissionDialogFragment() != null + } + + private fun hasExistingAddonPostInstallationDialogFragment(): Boolean { + return fragmentManager.findFragmentByTag(POST_INSTALLATION_DIALOG_FRAGMENT_TAG) + as? AddonInstallationDialogFragment != null + } + + private fun findPreviousPermissionDialogFragment(): PermissionsDialogFragment? { + return fragmentManager.findFragmentByTag(PERMISSIONS_DIALOG_FRAGMENT_TAG) as? PermissionsDialogFragment + } + + private fun findPreviousPostInstallationDialogFragment(): AddonInstallationDialogFragment? { + return fragmentManager.findFragmentByTag( + POST_INSTALLATION_DIALOG_FRAGMENT_TAG, + ) as? AddonInstallationDialogFragment + } + + private fun showPostInstallationDialog(addon: Addon) { + if (!isInstallationInProgress && !hasExistingAddonPostInstallationDialogFragment()) { + 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 = WeakReference(context) + + val dialog = AddonInstallationDialogFragment.newInstance( + addon = addon, + addonCollectionProvider = addonCollectionProvider, + promptsStyling = AddonInstallationDialogFragment.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + confirmButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context, + ), + confirmButtonTextColor = ThemeManager.resolveAttribute( + R.attr.textOnColorPrimary, + context, + ), + confirmButtonRadius = + (context.resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat(), + ), + onDismissed = { + consumePromptRequest() + }, + onConfirmButtonClicked = { _, allowInPrivateBrowsing -> + handlePostInstallationButtonClicked( + addon = addon, + context = weakApplicationContext, + allowInPrivateBrowsing = allowInPrivateBrowsing, + ) + }, + ) + dialog.show(fragmentManager, POST_INSTALLATION_DIALOG_FRAGMENT_TAG) + } + } + + private fun handlePostInstallationButtonClicked( + context: WeakReference, + allowInPrivateBrowsing: Boolean, + addon: Addon, + ) { + if (allowInPrivateBrowsing) { + context.get()?.components?.addonManager?.setAddonAllowedInPrivateBrowsing( + addon = addon, + allowed = true, + onSuccess = { updatedAddon -> + onAddonChanged(updatedAddon) + }, + ) + } + consumePromptRequest() + } + + companion object { + private const val PERMISSIONS_DIALOG_FRAGMENT_TAG = "ADDONS_PERMISSIONS_DIALOG_FRAGMENT" + private const val POST_INSTALLATION_DIALOG_FRAGMENT_TAG = + "ADDONS_INSTALLATION_DIALOG_FRAGMENT" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index a9b007f83..8bb16a46a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.MainScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.state.selector.findTab @@ -51,6 +52,7 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSitesConfig import mozilla.components.feature.top.sites.TopSitesFeature import mozilla.components.feature.top.sites.TopSitesFrecencyConfig @@ -59,7 +61,6 @@ import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.GleanMetrics.HomeScreen import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcutCfr import org.mozilla.fenix.HomeActivity @@ -351,6 +352,7 @@ class HomeFragment : Fragment() { viewLifecycleScope = viewLifecycleOwner.lifecycleScope, registerCollectionStorageObserver = ::registerCollectionStorageObserver, removeCollectionWithUndo = ::removeCollectionWithUndo, + showUndoSnackbarForTopSite = ::showUndoSnackbarForTopSite, showTabTray = ::openTabsTray, ), recentTabController = DefaultRecentTabsController( @@ -461,6 +463,24 @@ class HomeFragment : Fragment() { ) } + @VisibleForTesting + internal fun showUndoSnackbarForTopSite(topSite: TopSite) { + lifecycleScope.allowUndo( + view = requireView(), + message = getString(R.string.snackbar_top_site_removed), + undoActionTitle = getString(R.string.snackbar_deleted_undo), + onCancel = { + requireComponents.useCases.topSitesUseCase.addPinnedSites( + topSite.title.toString(), + topSite.url, + ) + }, + operation = { }, + elevation = TOAST_ELEVATION, + paddedForBottomToolbar = true, + ) + } + /** * The [SessionControlView] is forced to update with our current state when we call * [HomeFragment.onCreateView] in order to be able to draw everything at once with the current @@ -505,6 +525,9 @@ class HomeFragment : Fragment() { super.onViewCreated(view, savedInstanceState) HomeScreen.homeScreenDisplayed.record(NoExtras()) HomeScreen.homeScreenViewCount.add() + if (!browsingModeManager.mode.isPrivate) { + HomeScreen.standardHomepageViewCount.add() + } observeSearchEngineNameChanges() observeWallpaperUpdates() @@ -617,7 +640,7 @@ class HomeFragment : Fragment() { else -> null } } - .ifChanged() + .distinctUntilChanged() .collect { topSitesFeature.withFeature { it.storage.notifyObservers { onStorageUpdated() } diff --git a/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt index 516f68776..9060beaca 100644 --- a/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt +++ b/app/src/main/java/org/mozilla/fenix/home/collections/CollectionItem.kt @@ -4,31 +4,22 @@ package org.mozilla.fenix.home.collections -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.fillMaxWidth 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.DismissDirection.EndToStart import androidx.compose.material.DismissDirection.StartToEnd import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon import androidx.compose.material.SwipeToDismiss import androidx.compose.material.rememberDismissState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.res.painterResource @@ -42,6 +33,7 @@ import org.mozilla.fenix.R.drawable import org.mozilla.fenix.R.string import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.list.FaviconListItem +import org.mozilla.fenix.compose.tabstray.DismissedTabBackground import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.theme.FirefoxTheme import java.io.File @@ -83,7 +75,7 @@ fun CollectionItem( background = { DismissedTabBackground( dismissDirection = dismissState.dismissDirection, - isLastInCollection = isLastInCollection, + shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE, ) }, ) { @@ -122,56 +114,6 @@ fun CollectionItem( } } -/** - * Composable used to display the background of a [Tab] shown in collections that is being swiped left or right. - * - * @param dismissDirection [DismissDirection] of the tab being swiped depending on which this composable - * will also indicate the swipe direction by placing a warning icon at the start of the swipe gesture. - * If `null` the warning icon will be shown at both ends. - * @param isLastInCollection Whether the tab is to be shown between others or as the last one in collection. - */ -@Composable -private fun DismissedTabBackground( - dismissDirection: DismissDirection?, - isLastInCollection: Boolean, -) { - Card( - modifier = Modifier.fillMaxSize(), - backgroundColor = FirefoxTheme.colors.layer3, - shape = if (isLastInCollection) BOTTOM_TAB_SHAPE else MIDDLE_TAB_SHAPE, - elevation = 0.dp, - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = painterResource(drawable.ic_delete), - contentDescription = null, - modifier = Modifier - .padding(horizontal = 32.dp) - // Only show the delete icon for where the swipe starts. - .alpha( - if (dismissDirection == StartToEnd) 1f else 0f, - ), - tint = FirefoxTheme.colors.iconWarning, - ) - - Icon( - painter = painterResource(drawable.ic_delete), - contentDescription = null, - modifier = Modifier - .padding(horizontal = 32.dp) - // Only show the delete icon for where the swipe starts. - .alpha( - if (dismissDirection == EndToStart) 1f else 0f, - ), - tint = FirefoxTheme.colors.iconWarning, - ) - } - } -} - /** * Clips the Composable this applies to such that it cannot draw content / shadows outside it's top bound. */ @@ -194,12 +136,6 @@ private fun Modifier.clipTop() = this.then( private fun TabInCollectionPreview() { FirefoxTheme { Column { - Box(modifier = Modifier.height(56.dp)) { - DismissedTabBackground( - dismissDirection = StartToEnd, - isLastInCollection = false, - ) - } CollectionItem( tab = tabPreview, isLastInCollection = false, @@ -209,12 +145,6 @@ private fun TabInCollectionPreview() { Spacer(Modifier.height(10.dp)) - Box(modifier = Modifier.height(56.dp)) { - DismissedTabBackground( - dismissDirection = EndToStart, - isLastInCollection = true, - ) - } CollectionItem( tab = tabPreview, isLastInCollection = true, diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/HomeDeepLinkIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/HomeDeepLinkIntentProcessor.kt index 8900a0a9d..bf926efcf 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/HomeDeepLinkIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/HomeDeepLinkIntentProcessor.kt @@ -15,6 +15,7 @@ import mozilla.components.concept.engine.EngineSession import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.GleanMetrics.PlayStoreAttribution import org.mozilla.fenix.GlobalDirections import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -61,6 +62,10 @@ class HomeDeepLinkIntentProcessor( "settings_privacy" -> GlobalDirections.Settings "settings_wallpapers" -> GlobalDirections.WallpaperSettings "home_collections" -> GlobalDirections.Home + "test_deferred_deep_link" -> { + PlayStoreAttribution.deferredDeeplinkTime.stop() + return + } else -> return } diff --git a/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt index e22522e51..fa5cd45bb 100644 --- a/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt +++ b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt @@ -12,7 +12,6 @@ import android.text.Spanned import android.text.method.LinkMovementMethod import android.widget.TextView import androidx.appcompat.app.AlertDialog -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.MetricServiceType @@ -80,7 +79,7 @@ fun showPrivacyPopWindow(context: Context, activity: Activity) { .setTitle(context.getString(R.string.privacy_notice_title)) .setMessage(messageSpannable) .setCancelable(false) - val alertDialog: AlertDialog = builder.create().withCenterAlignedButtons() + val alertDialog: AlertDialog = builder.create() alertDialog.show() alertDialog.findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() } diff --git a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt index 653c2ad04..93efab52e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt +++ b/app/src/main/java/org/mozilla/fenix/home/pocket/PocketStoriesComposables.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.home.pocket +import android.content.res.Configuration import android.graphics.Rect import android.net.Uri import androidx.annotation.FloatRange @@ -39,6 +40,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView @@ -58,6 +60,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import kotlinx.coroutines.delay import mozilla.components.service.pocket.PocketStory import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory @@ -67,6 +70,7 @@ import mozilla.components.service.pocket.PocketStory.PocketSponsoredStoryShim import org.mozilla.fenix.R import org.mozilla.fenix.compose.ClickableSubstringLink import org.mozilla.fenix.compose.EagerFlingBehavior +import org.mozilla.fenix.compose.ITEM_WIDTH import org.mozilla.fenix.compose.ListItemTabLarge import org.mozilla.fenix.compose.ListItemTabLargePlaceholder import org.mozilla.fenix.compose.ListItemTabSurface @@ -271,12 +275,20 @@ fun PocketStories( val listState = rememberLazyListState() val flingBehavior = EagerFlingBehavior(lazyRowState = listState) + val configuration = LocalConfiguration.current + val screenWidth = configuration.screenWidthDp.dp + + val endPadding = + remember { mutableStateOf(endPadding(configuration, screenWidth, contentPadding)) } + // Force recomposition as padding is not consistently updated when orientation has changed. + endPadding.value = endPadding(configuration, screenWidth, contentPadding) + LazyRow( modifier = Modifier.semantics { testTagsAsResourceId = true testTag = "pocket.stories" }, - contentPadding = PaddingValues(horizontal = contentPadding), + contentPadding = PaddingValues(start = contentPadding, end = endPadding.value), state = listState, flingBehavior = flingBehavior, horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -330,6 +342,19 @@ fun PocketStories( } } +private fun endPadding(configuration: Configuration, screenWidth: Dp, contentPadding: Dp) = + if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + alignColumnToTitlePadding(screenWidth = screenWidth, contentPadding = contentPadding) + } else { + contentPadding + } + +/** + * If the column item is wider than the [screenWidth] default to the [contentPadding]. + */ +private fun alignColumnToTitlePadding(screenWidth: Dp, contentPadding: Dp) = + max(screenWidth - (ITEM_WIDTH.dp + contentPadding), contentPadding) + /** * Add a callback for when this Composable is "shown" on the screen. * This checks whether the composable has at least [threshold] ratio of it's total area drawn inside diff --git a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt index 8c7f60520..1bc8a68fd 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recentsyncedtabs/RecentSyncedTabFeature.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.home.recentsyncedtabs import android.content.Context import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import mozilla.components.browser.storage.sync.Tab @@ -23,7 +24,6 @@ import mozilla.components.service.fxa.store.SyncStore import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.telemetry.glean.GleanTimerId import org.mozilla.fenix.GleanMetrics.RecentSyncedTabs import org.mozilla.fenix.components.AppStore @@ -63,7 +63,7 @@ class RecentSyncedTabFeature( private fun collectAccountUpdates() { syncStore.flow() - .ifChanged { state -> + .distinctUntilChangedBy { state -> state.account != null }.onEach { state -> if (state.account != null) { @@ -82,7 +82,7 @@ class RecentSyncedTabFeature( private fun collectStatusUpdates() { syncStore.flow() - .ifChanged { state -> + .distinctUntilChangedBy { state -> state.status }.onEach { state -> when (state.status) { diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt index f5610ba31..63dd9a0be 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/RecentTabsListFeature.kt @@ -6,12 +6,12 @@ package org.mozilla.fenix.home.recenttabs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore 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.appstate.AppAction import org.mozilla.fenix.ext.asRecentTabs @@ -30,7 +30,7 @@ class RecentTabsListFeature( // Listen for changes regarding the currently selected tab and in progress media tab. flow .map { it.asRecentTabs() } - .ifChanged() + .distinctUntilChanged() .collect { appStore.dispatch(AppAction.RecentTabsChange(it)) } diff --git a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt index 29de1ad32..e26a7ff93 100644 --- a/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt +++ b/app/src/main/java/org/mozilla/fenix/home/recenttabs/view/RecentTabs.kt @@ -56,7 +56,7 @@ import org.mozilla.fenix.components.components import org.mozilla.fenix.compose.ContextualMenu import org.mozilla.fenix.compose.Image import org.mozilla.fenix.compose.MenuItem -import org.mozilla.fenix.compose.ThumbnailCard +import org.mozilla.fenix.compose.TabThumbnail import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.compose.inComposePreview import org.mozilla.fenix.home.recenttabs.RecentTab @@ -228,9 +228,8 @@ fun RecentTabImage( contentScale = ContentScale.Crop, ) } - else -> ThumbnailCard( - url = tab.state.content.url, - key = tab.state.id, + else -> TabThumbnail( + tab = tab.state, modifier = modifier, contentScale = contentScale, ) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index 4dd98bfd5..cc7add983 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -36,11 +36,17 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageVie import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.MessageCardViewHolder import org.mozilla.fenix.home.topsites.TopSitePagerViewHolder +import org.mozilla.fenix.home.topsites.TopSitesViewHolder import mozilla.components.feature.tab.collections.Tab as ComponentTab sealed class AdapterItem(@LayoutRes val viewType: Int) { object TopPlaceholderItem : AdapterItem(TopPlaceholderViewHolder.LAYOUT_ID) + /** + * Top sites. + */ + object TopSites : AdapterItem(TopSitesViewHolder.LAYOUT_ID) + /** * Contains a set of [Pair]s where [Pair.first] is the index of the changed [TopSite] and * [Pair.second] is the new [TopSite]. @@ -274,6 +280,11 @@ class SessionControlAdapter( viewLifecycleOwner = viewLifecycleOwner, interactor = interactor, ) + TopSitesViewHolder.LAYOUT_ID -> return TopSitesViewHolder( + composeView = ComposeView(parent.context), + viewLifecycleOwner = viewLifecycleOwner, + interactor = interactor, + ) } val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) @@ -320,6 +331,11 @@ class SessionControlAdapter( // This View already listens and maps store updates. Avoid creating and binding new Views. // The composition will live until the ViewTreeLifecycleOwner to which it's attached to is destroyed. } + is TopSitesViewHolder -> { + // Dispose the underlying composition immediately. + // This ViewHolder can be removed / re-added and we need it to show a fresh new composition. + holder.composeView.disposeComposition() + } is CollectionViewHolder -> { // Dispose the underlying composition immediately. // This ViewHolder can be removed / re-added and we need it to show a fresh new composition. @@ -381,6 +397,7 @@ class SessionControlAdapter( val (collection, tab, isLastTab) = item as AdapterItem.TabInCollectionItem holder.bindSession(collection, tab, isLastTab) } + is TopSitesViewHolder, is RecentlyVisitedViewHolder, is RecentBookmarksViewHolder, is RecentTabViewHolder, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index f2df81e51..b27d1664e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -28,7 +28,6 @@ import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSite import mozilla.components.service.nimbus.messaging.Message import mozilla.components.support.ktx.android.view.showKeyboard -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Collections @@ -193,6 +192,7 @@ class DefaultSessionControlController( private val viewLifecycleScope: CoroutineScope, private val registerCollectionStorageObserver: () -> Unit, private val removeCollectionWithUndo: (tabCollection: TabCollection) -> Unit, + private val showUndoSnackbarForTopSite: (topSite: TopSite) -> Unit, private val showTabTray: () -> Unit, ) : SessionControlController { @@ -312,7 +312,7 @@ class DefaultSessionControlController( setNegativeButton(R.string.top_sites_rename_dialog_cancel) { dialog, _ -> dialog.cancel() } - }.show().withCenterAlignedButtons().also { + }.show().also { topSiteLabelEditText.setSelection(0, topSiteLabelEditText.text.length) topSiteLabelEditText.showKeyboard() } @@ -332,6 +332,8 @@ class DefaultSessionControlController( removeTopSites(topSite) } } + + showUndoSnackbarForTopSite(topSite) } override fun handleRenameCollectionTapped(collection: TabCollection) { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt index 2f50a9494..03e37182a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlView.kt @@ -55,7 +55,11 @@ internal fun normalModeAdapterItems( } if (settings.showTopSitesFeature && topSites.isNotEmpty()) { - items.add(AdapterItem.TopSitePager(topSites)) + if (settings.enableComposeTopSites) { + items.add(AdapterItem.TopSites) + } else { + items.add(AdapterItem.TopSitePager(topSites)) + } } if (showRecentTab) { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionsMessageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionsMessageViewHolder.kt index 0d90c2230..5c93ab4a3 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionsMessageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/NoCollectionsMessageViewHolder.kt @@ -10,12 +10,12 @@ import androidx.compose.ui.graphics.toArgb import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.databinding.NoCollectionsMessageBinding @@ -53,7 +53,7 @@ class NoCollectionsMessageViewHolder( store.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.normalTabs.size } - .ifChanged() + .distinctUntilChanged() .collect { tabs -> binding.addTabsToCollectionsButton.isVisible = tabs > 0 } @@ -61,7 +61,7 @@ class NoCollectionsMessageViewHolder( appStore.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.wallpaperState } - .ifChanged() + .distinctUntilChanged() .collect { wallpaperState -> val textColor = wallpaperState.currentWallpaper.textColor if (textColor == null) { diff --git a/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt b/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt index 139921ef9..ac6943a73 100644 --- a/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorBinding.kt @@ -9,6 +9,7 @@ import android.graphics.drawable.BitmapDrawable import androidx.core.view.isGone import androidx.core.view.isVisible import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.state.BrowserState @@ -18,7 +19,6 @@ import mozilla.components.concept.menu.Orientation import mozilla.components.lib.state.helpers.AbstractBinding import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.GleanMetrics.UnifiedSearch import org.mozilla.fenix.R import org.mozilla.fenix.databinding.FragmentHomeBinding @@ -63,7 +63,7 @@ class SearchSelectorBinding( override suspend fun onState(flow: Flow) { flow.map { state -> state.search.selectedOrDefaultSearchEngine } - .ifChanged() + .distinctUntilChanged() .collect { searchEngine -> val name = searchEngine?.name val icon = searchEngine?.let { diff --git a/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt b/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt index 913b29f38..39b5cc91b 100644 --- a/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/home/toolbar/SearchSelectorMenuBinding.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.home.toolbar import android.content.Context import androidx.core.graphics.drawable.toDrawable import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.state.BrowserState @@ -15,7 +16,6 @@ import mozilla.components.concept.menu.candidate.DrawableMenuIcon import mozilla.components.concept.menu.candidate.TextMenuCandidate import mozilla.components.lib.state.helpers.AbstractBinding import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.search.ext.searchEngineShortcuts import org.mozilla.fenix.search.toolbar.SearchSelectorInteractor @@ -33,7 +33,7 @@ class SearchSelectorMenuBinding( override suspend fun onState(flow: Flow) { flow.map { state -> state.search } - .ifChanged() + .distinctUntilChanged() .collect { search -> updateSearchSelectorMenu(search.searchEngineShortcuts) } diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt index 523c27101..d69f27e57 100644 --- a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSiteItemViewHolder.kt @@ -20,13 +20,13 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.feature.top.sites.TopSite import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.GleanMetrics.Pings import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.R @@ -97,7 +97,7 @@ class TopSiteItemViewHolder( appStore.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.wallpaperState } - .ifChanged() + .distinctUntilChanged() .collect { currentState -> var backgroundColor = ContextCompat.getColor(view.context, R.color.fx_mobile_layer_color_2) diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt new file mode 100644 index 000000000..7dd72c4e1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSites.kt @@ -0,0 +1,539 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.topsites + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +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.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.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Card +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.ContextualMenu +import org.mozilla.fenix.compose.Favicon +import org.mozilla.fenix.compose.MenuItem +import org.mozilla.fenix.compose.PagerIndicator +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.ext.bitmapForUrl +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.theme.FirefoxTheme +import org.mozilla.fenix.wallpapers.WallpaperState +import kotlin.math.ceil + +private const val TOP_SITES_PER_PAGE = 8 +private const val TOP_SITES_PER_ROW = 4 +private const val TOP_SITES_ITEM_SIZE = 84 +private const val TOP_SITES_ROW_WIDTH = TOP_SITES_PER_ROW * TOP_SITES_ITEM_SIZE +private const val TOP_SITES_FAVICON_CARD_SIZE = 60 +private const val TOP_SITES_FAVICON_SIZE = 36 + +/** + * A list of top sites. + * + * @param topSites List of [TopSite] to display. + * @param topSiteColors The color set defined by [TopSiteColors] used to style a top site. + * @param onTopSiteClick Invoked when the user clicks on a top site. + * @param onTopSiteLongClick Invoked when the user long clicks on a top site. + * @param onOpenInPrivateTabClicked Invoked when the user clicks on the "Open in private tab" + * menu item. + * @param onRenameTopSiteClicked Invoked when the user clicks on the "Rename" menu item. + * @param onRemoveTopSiteClicked Invoked when the user clicks on the "Remove" menu item. + * @param onSettingsClicked Invoked when the user clicks on the "Settings" menu item. + * @param onSponsorPrivacyClicked Invoked when the user clicks on the "Our sponsors & your privacy" + * menu item. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +@Suppress("LongParameterList") +fun TopSites( + topSites: List, + topSiteColors: TopSiteColors = TopSiteColors.colors(), + onTopSiteClick: (TopSite) -> Unit, + onTopSiteLongClick: (TopSite) -> Unit, + onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit, + onRenameTopSiteClicked: (topSite: TopSite) -> Unit, + onRemoveTopSiteClicked: (topSite: TopSite) -> Unit, + onSettingsClicked: () -> Unit, + onSponsorPrivacyClicked: () -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val pagerState = rememberPagerState() + val pageCount = ceil((topSites.size.toDouble() / TOP_SITES_PER_PAGE)).toInt() + + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + HorizontalPager( + pageCount = pageCount, + state = pagerState, + ) { page -> + Column { + val topSitesWindows = topSites.windowed( + size = TOP_SITES_PER_PAGE, + step = TOP_SITES_PER_PAGE, + partialWindows = true, + )[page].chunked(TOP_SITES_PER_ROW) + + for (items in topSitesWindows) { + Row(modifier = Modifier.defaultMinSize(minWidth = TOP_SITES_ROW_WIDTH.dp)) { + items.forEach { topSite -> + TopSiteItem( + topSite = topSite, + menuItems = getMenuItems( + topSite = topSite, + onOpenInPrivateTabClicked = onOpenInPrivateTabClicked, + onRenameTopSiteClicked = onRenameTopSiteClicked, + onRemoveTopSiteClicked = onRemoveTopSiteClicked, + onSettingsClicked = onSettingsClicked, + onSponsorPrivacyClicked = onSponsorPrivacyClicked, + ), + topSiteColors = topSiteColors, + onTopSiteClick = { item -> onTopSiteClick(item) }, + onTopSiteLongClick = onTopSiteLongClick, + ) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + } + } + } + } + + if (pageCount > 1) { + Spacer(modifier = Modifier.height(8.dp)) + + PagerIndicator( + pagerState = pagerState, + pageCount = pageCount, + modifier = Modifier.padding(horizontal = 16.dp), + spacing = 4.dp, + ) + } + } +} + +/** + * Represents the colors used by top sites. + */ +data class TopSiteColors( + val titleTextColor: Color, + val sponsoredTextColor: Color, + val faviconCardBackgroundColor: Color, +) { + companion object { + /** + * Builder function used to construct an instance of [TopSiteColors]. + */ + @Composable + fun colors( + titleTextColor: Color = FirefoxTheme.colors.textPrimary, + sponsoredTextColor: Color = FirefoxTheme.colors.textSecondary, + faviconCardBackgroundColor: Color = FirefoxTheme.colors.layer2, + ) = TopSiteColors( + titleTextColor = titleTextColor, + sponsoredTextColor = sponsoredTextColor, + faviconCardBackgroundColor = faviconCardBackgroundColor, + ) + + /** + * Builder function used to construct an instance of [TopSiteColors] given a + * [WallpaperState]. + */ + @Composable + fun colors(wallpaperState: WallpaperState): TopSiteColors { + val textColor: Long? = wallpaperState.currentWallpaper.textColor + val (titleTextColor, sponsoredTextColor) = if (textColor == null) { + FirefoxTheme.colors.textPrimary to FirefoxTheme.colors.textSecondary + } else { + Color(textColor) to Color(textColor) + } + + var faviconCardBackgroundColor = FirefoxTheme.colors.layer2 + + wallpaperState.composeRunIfWallpaperCardColorsAreAvailable { cardColorLight, cardColorDark -> + faviconCardBackgroundColor = if (isSystemInDarkTheme()) { + cardColorDark + } else { + cardColorLight + } + } + + return TopSiteColors( + titleTextColor = titleTextColor, + sponsoredTextColor = sponsoredTextColor, + faviconCardBackgroundColor = faviconCardBackgroundColor, + ) + } + } +} + +/** + * A top site item. + * + * @param topSite The [TopSite] to display. + * @param menuItems List of [MenuItem]s to display in a top site dropdown menu. + * @param topSiteColors The color set defined by [TopSiteColors] used to style a top site. + * @param onTopSiteClick Invoked when the user clicks on a top site. + * @param onTopSiteLongClick Invoked when the user long clicks on a top site. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun TopSiteItem( + topSite: TopSite, + menuItems: List, + topSiteColors: TopSiteColors, + onTopSiteClick: (TopSite) -> Unit, + onTopSiteLongClick: (TopSite) -> Unit, +) { + var menuExpanded by remember { mutableStateOf(false) } + + Box { + Column( + modifier = Modifier + .combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { onTopSiteClick(topSite) }, + onLongClick = { + onTopSiteLongClick(topSite) + menuExpanded = true + }, + ) + .width(TOP_SITES_ITEM_SIZE.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(4.dp)) + + TopSiteFaviconCard( + topSite = topSite, + backgroundColor = topSiteColors.faviconCardBackgroundColor, + ) + + Spacer(modifier = Modifier.height(6.dp)) + + Row( + modifier = Modifier.width(TOP_SITES_ITEM_SIZE.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + if (topSite is TopSite.Pinned || topSite is TopSite.Default) { + Image( + painter = painterResource(id = R.drawable.ic_new_pin), + contentDescription = null, + ) + + Spacer(modifier = Modifier.width(2.dp)) + } + + Text( + text = topSite.title ?: topSite.url, + color = topSiteColors.titleTextColor, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = FirefoxTheme.typography.caption, + ) + } + + Text( + text = stringResource(id = R.string.top_sites_sponsored_label), + modifier = Modifier + .width(TOP_SITES_ITEM_SIZE.dp) + .alpha(alpha = if (topSite is TopSite.Provided) 1f else 0f), + color = topSiteColors.sponsoredTextColor, + fontSize = 10.sp, + textAlign = TextAlign.Center, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + + ContextualMenu( + menuItems = menuItems, + showMenu = menuExpanded, + onDismissRequest = { menuExpanded = false }, + ) + } +} + +/** + * The top site favicon card. + * + * @param topSite The [TopSite] to display. + * @param backgroundColor The background [Color] of the card. + */ +@Composable +private fun TopSiteFaviconCard( + topSite: TopSite, + backgroundColor: Color, +) { + Card( + modifier = Modifier.size(TOP_SITES_FAVICON_CARD_SIZE.dp), + shape = RoundedCornerShape(8.dp), + backgroundColor = backgroundColor, + elevation = 6.dp, + ) { + Box(contentAlignment = Alignment.Center) { + Surface( + modifier = Modifier.size(TOP_SITES_FAVICON_SIZE.dp), + color = backgroundColor, + shape = RoundedCornerShape(4.dp), + ) { + val drawableForUrl = getDrawableForUrl(topSite.url) + when { + drawableForUrl != null -> { + FaviconImage(painterResource(drawableForUrl)) + } + + topSite is TopSite.Provided -> { + FaviconBitmap(topSite) + } + + else -> { + FaviconDefault(topSite.url) + } + } + } + } + } +} + +@Composable +private fun FaviconImage(painter: Painter) { + Image( + painter = painter, + contentDescription = null, + modifier = Modifier.size(TOP_SITES_FAVICON_SIZE.dp), + contentScale = ContentScale.Crop, + ) +} + +@Composable +private fun FaviconBitmap(topSite: TopSite.Provided) { + var faviconBitmapUiState by remember { mutableStateOf(FaviconBitmapUiState.Loading) } + + val client = LocalContext.current.components.core.client + + LaunchedEffect(topSite.imageUrl) { + val bitmapForUrl = client.bitmapForUrl(topSite.imageUrl) + + faviconBitmapUiState = if (bitmapForUrl == null) { + FaviconBitmapUiState.Error + } else { + FaviconBitmapUiState.Data(bitmapForUrl.asImageBitmap()) + } + } + + when (val uiState = faviconBitmapUiState) { + is FaviconBitmapUiState.Loading, FaviconBitmapUiState.Error -> FaviconDefault(topSite.url) + is FaviconBitmapUiState.Data -> FaviconImage(BitmapPainter(uiState.imageBitmap)) + } +} + +private sealed class FaviconBitmapUiState { + data class Data(val imageBitmap: ImageBitmap) : FaviconBitmapUiState() + object Loading : FaviconBitmapUiState() + object Error : FaviconBitmapUiState() +} + +@Composable +private fun FaviconDefault(url: String) { + Favicon(url = url, size = TOP_SITES_FAVICON_SIZE.dp) +} + +private fun getDrawableForUrl(url: String) = + when (url) { + SupportUtils.POCKET_TRENDING_URL -> R.drawable.ic_pocket + SupportUtils.BAIDU_URL -> R.drawable.ic_baidu + SupportUtils.JD_URL -> R.drawable.ic_jd + SupportUtils.PDD_URL -> R.drawable.ic_pdd + SupportUtils.TC_URL -> R.drawable.ic_tc + SupportUtils.MEITUAN_URL -> R.drawable.ic_meituan + else -> null + } + +@Composable +@Suppress("LongParameterList") +private fun getMenuItems( + topSite: TopSite, + onOpenInPrivateTabClicked: (topSite: TopSite) -> Unit, + onRenameTopSiteClicked: (topSite: TopSite) -> Unit, + onRemoveTopSiteClicked: (topSite: TopSite) -> Unit, + onSettingsClicked: () -> Unit, + onSponsorPrivacyClicked: () -> Unit, +): List { + val isPinnedSite = topSite is TopSite.Pinned || topSite is TopSite.Default + val isProvidedSite = topSite is TopSite.Provided + val result = mutableListOf() + + result.add( + MenuItem( + title = stringResource(id = R.string.bookmark_menu_open_in_private_tab_button), + onClick = { onOpenInPrivateTabClicked(topSite) }, + ), + ) + + if (isPinnedSite) { + result.add( + MenuItem( + title = stringResource(id = R.string.rename_top_site), + onClick = { onRenameTopSiteClicked(topSite) }, + ), + ) + } + + if (!isProvidedSite) { + result.add( + MenuItem( + title = stringResource( + id = if (isPinnedSite) { + R.string.remove_top_site + } else { + R.string.delete_from_history + }, + ), + onClick = { onRemoveTopSiteClicked(topSite) }, + ), + ) + } + + if (isProvidedSite) { + result.add( + MenuItem( + title = stringResource(id = R.string.delete_from_history), + onClick = { onRemoveTopSiteClicked(topSite) }, + ), + ) + } + + if (isProvidedSite) { + result.add( + MenuItem( + title = stringResource(id = R.string.top_sites_menu_settings), + onClick = onSettingsClicked, + ), + ) + } + + if (isProvidedSite) { + result.add( + MenuItem( + title = stringResource(id = R.string.top_sites_menu_sponsor_privacy), + onClick = onSponsorPrivacyClicked, + ), + ) + } + + return result +} + +@Composable +@LightDarkPreview +private fun TopSitesPreview() { + FirefoxTheme { + Box(modifier = Modifier.background(color = FirefoxTheme.colors.layer1)) { + TopSites( + topSites = mutableListOf().apply { + for (index in 0 until 2) { + add( + TopSite.Pinned( + id = index.toLong(), + title = "Mozilla$index", + url = "mozilla.com", + createdAt = 0L, + ), + ) + } + + for (index in 0 until 2) { + add( + TopSite.Provided( + id = index.toLong(), + title = "Mozilla$index", + url = "mozilla.com", + clickUrl = "https://mozilla.com/click", + imageUrl = "https://test.com/image2.jpg", + impressionUrl = "https://example.com", + createdAt = 0L, + ), + ) + } + + for (index in 0 until 2) { + add( + TopSite.Default( + id = index.toLong(), + title = "Mozilla$index", + url = "mozilla.com", + createdAt = 0L, + ), + ) + } + + add( + TopSite.Default( + id = null, + title = "Top Articles", + url = "https://getpocket.com/fenix-top-articles", + createdAt = 0L, + ), + ) + }, + onTopSiteClick = {}, + onTopSiteLongClick = {}, + onOpenInPrivateTabClicked = {}, + onRenameTopSiteClicked = {}, + onRemoveTopSiteClicked = {}, + onSettingsClicked = {}, + onSponsorPrivacyClicked = {}, + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt new file mode 100644 index 000000000..ee8322643 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/topsites/TopSitesViewHolder.kt @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.topsites + +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.LifecycleOwner +import mozilla.components.lib.state.ext.observeAsState +import org.mozilla.fenix.components.components +import org.mozilla.fenix.compose.ComposeViewHolder +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor +import org.mozilla.fenix.wallpapers.WallpaperState + +/** + * View holder for top sites. + * + * @param composeView [ComposeView] which will be populated with Jetpack Compose UI content. + * @param viewLifecycleOwner [LifecycleOwner] to which this Composable will be tied to. + * @param interactor [TopSiteInteractor] which will have delegated to all user top sites + * interactions. + */ +class TopSitesViewHolder( + composeView: ComposeView, + viewLifecycleOwner: LifecycleOwner, + private val interactor: TopSiteInteractor, +) : ComposeViewHolder(composeView, viewLifecycleOwner) { + + @Composable + override fun Content() { + val topSites = + components.appStore.observeAsState(emptyList()) { state -> state.topSites }.value + val wallpaperState = components.appStore + .observeAsState(WallpaperState.default) { state -> state.wallpaperState }.value + + TopSites( + topSites = topSites, + topSiteColors = TopSiteColors.colors(wallpaperState = wallpaperState), + onTopSiteClick = { topSite -> + interactor.onSelectTopSite(topSite, topSites.indexOf(topSite)) + }, + onTopSiteLongClick = interactor::onTopSiteLongClicked, + onOpenInPrivateTabClicked = interactor::onOpenInPrivateTabClicked, + onRenameTopSiteClicked = interactor::onRenameTopSiteClicked, + onRemoveTopSiteClicked = interactor::onRemoveTopSiteClicked, + onSettingsClicked = interactor::onSettingsClicked, + onSponsorPrivacyClicked = interactor::onSponsorPrivacyClicked, + ) + } + + companion object { + val LAYOUT_ID = View.generateViewId() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 9ddd6f9fd..2f9c498d6 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -40,7 +40,6 @@ import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.kotlin.toShortUrl -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.HomeActivity @@ -52,7 +51,6 @@ import org.mozilla.fenix.databinding.FragmentBookmarkBinding import org.mozilla.fenix.ext.bookmarkStorage import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView -import org.mozilla.fenix.ext.minus import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.setTextColor @@ -306,7 +304,7 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan dialog.dismiss() } setCancelable(false) - create().withCenterAlignedButtons() + create() show() } } @@ -411,7 +409,7 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan operation = getDeleteOperation(BookmarkRemoveType.FOLDER), ) } - create().withCenterAlignedButtons() + create() } .show() } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt index a0da5d34d..6ebb055f8 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentInteractor.kt @@ -9,7 +9,7 @@ import mozilla.components.concept.storage.BookmarkNodeType import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.utils.Do +import org.mozilla.fenix.components.metrics.MetricsUtils /** * Interactor for the Bookmarks screen. @@ -34,6 +34,10 @@ class BookmarkFragmentInteractor( override fun onEditPressed(node: BookmarkNode) { bookmarksController.handleBookmarkEdit(node) + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.EDIT, + METRIC_SOURCE, + ) } override fun onAllBookmarksDeselected() { @@ -70,6 +74,10 @@ class BookmarkFragmentInteractor( bookmarksController.handleOpeningBookmark(item, BrowsingMode.Normal) BookmarksManagement.openInNewTab.record(NoExtras()) } + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.OPEN, + METRIC_SOURCE, + ) } override fun onOpenInPrivateTab(item: BookmarkNode) { @@ -78,16 +86,28 @@ class BookmarkFragmentInteractor( bookmarksController.handleOpeningBookmark(item, BrowsingMode.Private) BookmarksManagement.openInPrivateTab.record(NoExtras()) } + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.OPEN, + METRIC_SOURCE, + ) } override fun onOpenAllInNewTabs(folder: BookmarkNode) { require(folder.type == BookmarkNodeType.FOLDER) bookmarksController.handleOpeningFolderBookmarks(folder, BrowsingMode.Normal) + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.OPEN, + METRIC_SOURCE, + ) } override fun onOpenAllInPrivateTabs(folder: BookmarkNode) { require(folder.type == BookmarkNodeType.FOLDER) bookmarksController.handleOpeningFolderBookmarks(folder, BrowsingMode.Private) + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.OPEN, + METRIC_SOURCE, + ) } override fun onDelete(nodes: Set) { @@ -101,6 +121,10 @@ class BookmarkFragmentInteractor( BookmarkNodeType.FOLDER -> BookmarkRemoveType.FOLDER null -> BookmarkRemoveType.MULTIPLE } + MetricsUtils.recordBookmarkMetrics( + MetricsUtils.BookmarkAction.DELETE, + METRIC_SOURCE, + ) if (eventType == BookmarkRemoveType.FOLDER) { bookmarksController.handleBookmarkFolderDeletion(nodes) } else { @@ -113,7 +137,7 @@ class BookmarkFragmentInteractor( } override fun open(item: BookmarkNode) { - Do exhaustive when (item.type) { + when (item.type) { BookmarkNodeType.ITEM -> { bookmarksController.handleBookmarkTapped(item) BookmarksManagement.open.record(NoExtras()) @@ -134,4 +158,8 @@ class BookmarkFragmentInteractor( override fun onRequestSync() { bookmarksController.handleRequestSync() } + + companion object { + const val METRIC_SOURCE = "bookmark_panel" + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkSearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkSearchDialogFragment.kt index 71acc1384..eb288b072 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkSearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkSearchDialogFragment.kt @@ -29,7 +29,7 @@ import androidx.constraintlayout.widget.ConstraintProperties.TOP import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.toolbar.BrowserToolbar @@ -37,7 +37,6 @@ import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.android.view.hideKeyboard -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -186,7 +185,7 @@ class BookmarkSearchDialogFragment : AppCompatDialogFragment(), UserInteractionH private fun observeAwesomeBarState() = consumeFlow(store) { flow -> flow.map { state -> state.query.isNotBlank() } - .ifChanged() + .distinctUntilChanged() .collect { shouldShowAwesomebar -> binding.awesomeBar.visibility = if (shouldShowAwesomebar) { View.VISIBLE diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt index 235c3427d..27a551f84 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/edit/EditBookmarkFragment.kt @@ -37,12 +37,12 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.kotlin.toShortUrl -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.databinding.FragmentEditBookmarkBinding import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView @@ -222,6 +222,7 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark), MenuProv lifecycleScope.launch(IO) { requireComponents.core.bookmarksStorage.deleteNode(args.guidToEdit) BookmarksManagement.removed.record(NoExtras()) + MetricsUtils.recordBookmarkMetrics(MetricsUtils.BookmarkAction.DELETE, METRIC_SOURCE) launch(Main) { Navigation.findNavController(requireActivity(), R.id.container) @@ -245,7 +246,7 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark), MenuProv } dialog.dismiss() } - create().withCenterAlignedButtons() + create() }.show() } } @@ -312,4 +313,8 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark), MenuProv _binding = null } + + companion object { + private const val METRIC_SOURCE = "bookmark_edit_page" + } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/viewholders/BookmarkNodeViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/viewholders/BookmarkNodeViewHolder.kt index 265aaa981..0e5c70f74 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/viewholders/BookmarkNodeViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/viewholders/BookmarkNodeViewHolder.kt @@ -23,7 +23,6 @@ import org.mozilla.fenix.library.bookmarks.BookmarkItemMenu import org.mozilla.fenix.library.bookmarks.BookmarkPayload import org.mozilla.fenix.library.bookmarks.BookmarkViewInteractor import org.mozilla.fenix.library.bookmarks.inRoots -import org.mozilla.fenix.utils.Do /** * Base class for bookmark node view holders. @@ -39,7 +38,7 @@ class BookmarkNodeViewHolder( init { menu = BookmarkItemMenu(containerView.context) { menuItem -> val item = this.item ?: return@BookmarkItemMenu - Do exhaustive when (menuItem) { + when (menuItem) { BookmarkItemMenu.Item.Edit -> interactor.onEditPressed(item) BookmarkItemMenu.Item.Copy -> interactor.onCopyPressed(item) BookmarkItemMenu.Item.Share -> interactor.onSharePressed(item) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index a1962bbf6..c518ee0a3 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -39,7 +39,6 @@ import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.kotlin.toShortUrl -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity @@ -421,7 +420,7 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandler, } GleanHistory.removePromptOpened.record(NoExtras()) - }.create().withCenterAlignedButtons() + }.create() } @Suppress("UnusedPrivateMember") diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistorySearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistorySearchDialogFragment.kt index f9785c6aa..4c524be17 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistorySearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistorySearchDialogFragment.kt @@ -29,7 +29,7 @@ import androidx.constraintlayout.widget.ConstraintProperties.TOP import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.toolbar.BrowserToolbar @@ -37,7 +37,6 @@ import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.android.view.hideKeyboard -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -186,7 +185,7 @@ class HistorySearchDialogFragment : AppCompatDialogFragment(), UserInteractionHa private fun observeAwesomeBarState() = consumeFlow(store) { flow -> flow.map { state -> state.query.isNotBlank() } - .ifChanged() + .distinctUntilChanged() .collect { shouldShowAwesomebar -> binding.awesomeBar.visibility = if (shouldShowAwesomebar) { View.VISIBLE diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index 6d8226f5e..5c8eaab39 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -17,7 +17,6 @@ import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryInteractor import org.mozilla.fenix.library.history.HistoryItemTimeGroup import org.mozilla.fenix.selection.SelectionHolder -import org.mozilla.fenix.utils.Do class HistoryListItemViewHolder( view: View, @@ -67,7 +66,7 @@ class HistoryListItemViewHolder( binding.historyLayout.titleView.text = item.title - binding.historyLayout.urlView.text = Do exhaustive when (item) { + binding.historyLayout.urlView.text = when (item) { is History.Regular -> item.url is History.Metadata -> item.url is History.Group -> { diff --git a/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt index 49891f1c9..34a3a94d2 100644 --- a/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/historymetadata/HistoryMetadataGroupFragment.kt @@ -28,7 +28,6 @@ import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.kotlin.toShortUrl -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.addons.showSnackBar @@ -276,7 +275,7 @@ class HistoryMetadataGroupFragment : interactor.onDeleteAllConfirmed() dialog.dismiss() } - .create().withCenterAlignedButtons() + .create() companion object { const val TAG = "DELETE_CONFIRMATION_DIALOG_FRAGMENT" diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt index 14abd9837..593a9d367 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -16,13 +16,12 @@ import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.base.feature.UserInteractionHandler -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.RecentlyClosedTabs @@ -158,7 +157,7 @@ class RecentlyClosedFragment : requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.closedTabs } - .ifChanged() + .distinctUntilChanged() .collect { tabs -> recentlyClosedFragmentStore.dispatch( RecentlyClosedFragmentAction.Change(tabs), diff --git a/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt b/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt index 62a7ca270..4facd60db 100644 --- a/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/messaging/CustomAttributeProvider.kt @@ -7,15 +7,15 @@ package org.mozilla.fenix.messaging import android.content.Context import androidx.core.app.NotificationManagerCompat import mozilla.components.service.nimbus.messaging.JexlAttributeProvider +import mozilla.components.support.base.ext.areNotificationsEnabledSafe +import mozilla.components.support.utils.BrowsersCache import org.json.JSONObject import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_CAMPAIGN import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_CONTENT import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_MEDIUM import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_SOURCE import org.mozilla.fenix.components.metrics.UTMParams.Companion.UTM_TERM -import org.mozilla.fenix.ext.areNotificationsEnabledSafe import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.utils.BrowsersCache import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt index 94be2c1fa..640d82955 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/JunoOnboardingFragment.kt @@ -19,9 +19,9 @@ import androidx.compose.ui.platform.LocalContext import androidx.core.app.NotificationManagerCompat import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController +import mozilla.components.support.base.ext.areNotificationsEnabledSafe import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint -import org.mozilla.fenix.ext.areNotificationsEnabledSafe import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.openSetDefaultBrowserOption diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt index 1015cd597..84f1c3805 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt @@ -12,6 +12,9 @@ import android.widget.ImageView import androidx.appcompat.widget.AppCompatRadioButton import androidx.core.content.edit import androidx.core.content.withStyledAttributes +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import org.mozilla.fenix.R import org.mozilla.fenix.ext.setTextColor import org.mozilla.fenix.ext.setTextSize @@ -91,8 +94,10 @@ class OnboardingRadioButton( illustration?.let { it.isSelected = isChecked } - context.settings().preferences.edit { - putBoolean(context.getString(key), isChecked) + CoroutineScope(Dispatchers.IO).launch { + context.settings().preferences.edit { + putBoolean(context.getString(key), isChecked) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt b/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt index 96b541fa8..ef8ce2c29 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/ReEngagementNotificationWorker.kt @@ -37,8 +37,9 @@ class ReEngagementNotificationWorker( override fun doWork(): Result { val settings = applicationContext.settings() + val isActiveUser = isActiveUser(settings.lastBrowseActivity, System.currentTimeMillis()) - if (isActiveUser(settings) || !settings.shouldShowReEngagementNotification()) { + if (isActiveUser || !settings.shouldShowReEngagementNotification()) { return Result.success() } @@ -136,8 +137,8 @@ class ReEngagementNotificationWorker( } @VisibleForTesting - internal fun isActiveUser(settings: Settings): Boolean { - if (System.currentTimeMillis() - settings.lastBrowseActivity > INACTIVE_USER_THRESHOLD) { + internal fun isActiveUser(lastBrowseActivity: Long, currentTimeMillis: Long): Boolean { + if (currentTimeMillis - lastBrowseActivity > INACTIVE_USER_THRESHOLD) { return false } diff --git a/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt b/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt index 1b54f1589..b49ec71fd 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt @@ -69,7 +69,7 @@ class AppStartReasonProvider { // this Runnable should execute before the first Activity is created. reason = when (reason) { StartReason.TO_BE_DETERMINED -> StartReason.NON_ACTIVITY - StartReason.ACTIVITY -> reason /* the start reason is already known: do nothing. */ + StartReason.ACTIVITY -> reason // the start reason is already known: do nothing. StartReason.NON_ACTIVITY -> { Metrics.startReasonProcessError.set(true) logger.error("AppStartReasonProvider.Process...onCreate unexpectedly called twice") @@ -87,7 +87,7 @@ class AppStartReasonProvider { // See ProcessLifecycleObserver.onCreate for details. reason = when (reason) { StartReason.TO_BE_DETERMINED -> StartReason.ACTIVITY - StartReason.NON_ACTIVITY -> reason /* the start reason is already known: do nothing. */ + StartReason.NON_ACTIVITY -> reason // the start reason is already known: do nothing. StartReason.ACTIVITY -> { Metrics.startReasonActivityError.set(true) logger.error("AppStartReasonProvider.Activity...onCreate unexpectedly called twice") diff --git a/app/src/main/java/org/mozilla/fenix/perf/HomeActivityRootLinearLayout.kt b/app/src/main/java/org/mozilla/fenix/perf/HomeActivityRootLinearLayout.kt index f3a6ad49b..2f07cfa69 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/HomeActivityRootLinearLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/HomeActivityRootLinearLayout.kt @@ -33,7 +33,7 @@ class HomeActivityRootLinearLayout(context: Context, attrs: AttributeSet) : Line profiler?.addMarker(MEASURE_LAYOUT_DRAW_MARKER_NAME, profilerStartTime, "onLayout (HomeActivity root)") } - override fun dispatchDraw(canvas: Canvas?) { + override fun dispatchDraw(canvas: Canvas) { // We instrument dispatchDraw, for drawing children, because LinearLayout never draws itself, // i.e. it never calls onDraw or draw. val profilerStartTime = profiler?.getProfilerTime() diff --git a/app/src/main/java/org/mozilla/fenix/perf/SearchDialogFragmentConstraintLayout.kt b/app/src/main/java/org/mozilla/fenix/perf/SearchDialogFragmentConstraintLayout.kt index 3ef89b40f..2191da5e5 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/SearchDialogFragmentConstraintLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/SearchDialogFragmentConstraintLayout.kt @@ -32,7 +32,7 @@ class SearchDialogFragmentConstraintLayout(context: Context, attrs: AttributeSet profiler?.addMarker(MEASURE_LAYOUT_DRAW_MARKER_NAME, profilerStartTime, "onLayout (SearchDialogFragment root)") } - override fun draw(canvas: Canvas?) { + override fun draw(canvas: Canvas) { // We instrument draw, rather than onDraw or dispatchDraw, because ConstraintLayout's draw includes // both of the other methods. If we want to track how long it takes to draw the children, // we'd get more information by instrumenting them individually. diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt index 89018d378..f926d0df5 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt @@ -18,7 +18,6 @@ import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.ktx.kotlin.isUrl -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.SearchShortcuts @@ -319,7 +318,7 @@ class SearchDialogController( dialog.cancel() activity.startActivity(intent) } - create().withCenterAlignedButtons() + create() } } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index bf03f809d..e9377a221 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -43,6 +43,7 @@ import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.state.search.SearchEngine @@ -69,9 +70,7 @@ import mozilla.components.support.ktx.android.view.findViewInHierarchy import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.kotlin.toNormalizedUrl import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.ui.autocomplete.InlineAutocompleteEditText -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Awesomebar import org.mozilla.fenix.GleanMetrics.VoiceSearch @@ -331,7 +330,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { consumeFlow(requireComponents.core.store) { flow -> flow.map { state -> state.search } - .ifChanged() + .distinctUntilChanged() .collect { search -> store.dispatch( SearchFragmentAction.UpdateSearchState( @@ -364,6 +363,12 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { false } } + R.id.onboardingFragment -> { + binding.searchWrapper.setOnTouchListener { _, _ -> + dismissAllowingStateLoss() + true + } + } R.id.historyFragment, R.id.bookmarkFragment -> { binding.searchWrapper.setOnTouchListener { _, _ -> dismissAllowingStateLoss() @@ -540,7 +545,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private fun observeSuggestionProvidersState() = consumeFlow(store) { flow -> flow.map { state -> state.toSearchProviderState() } - .ifChanged() + .distinctUntilChanged() .collect { state -> awesomeBarView.updateSuggestionProvidersVisibility(state) } } @@ -557,7 +562,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { * */ flow.map { state -> state.url != state.query && state.query.isNotBlank() || state.showSearchShortcuts } - .ifChanged() + .distinctUntilChanged() .collect { shouldShowAwesomebar -> binding.awesomeBar.visibility = if (shouldShowAwesomebar) { View.VISIBLE @@ -574,7 +579,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { state.clipboardHasUrl && !state.showSearchShortcuts Pair(shouldShowView, state.clipboardHasUrl) } - .ifChanged() + .distinctUntilChanged() .collect { (shouldShowView) -> updateClipboardSuggestion(shouldShowView) } @@ -689,7 +694,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { setPositiveButton(R.string.qr_scanner_dialog_invalid_ok) { dialog: DialogInterface, _ -> dialog.dismiss() } - create().withCenterAlignedButtons() + create() }.show() } } else { @@ -714,7 +719,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { ) dialog.dismiss() } - create().withCenterAlignedButtons() + create() }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt index a508b781d..57f5a7243 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragmentStore.kt @@ -261,7 +261,8 @@ private fun searchStateReducer(state: SearchFragmentState, action: SearchFragmen state.copy( searchEngineSource = SearchEngineSource.Default(action.engine), showSearchSuggestions = shouldShowSearchSuggestions(action.browsingMode, action.settings), - showSearchShortcuts = action.settings.shouldShowSearchShortcuts, + showSearchShortcuts = action.settings.shouldShowSearchShortcuts && + !action.settings.showUnifiedSearchFeature, showClipboardSuggestions = action.settings.shouldShowClipboardSuggestions, showSearchTermHistory = action.settings.showUnifiedSearchFeature && action.settings.shouldShowHistorySuggestions, diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index fa1b8917b..2286fa723 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -493,8 +493,8 @@ class AwesomeBarView( searchEngineSource: SearchEngineSource, filterByCurrentEngine: Boolean = false, ): SessionSuggestionProvider { - val searchEngineHostFilter = when (filterByCurrentEngine) { - true -> searchEngineSource.searchEngine?.resultsUrl?.host + val searchEngineUriFilter = when (filterByCurrentEngine) { + true -> searchEngineSource.searchEngine?.resultsUrl false -> null } @@ -506,7 +506,7 @@ class AwesomeBarView( getDrawable(activity, R.drawable.ic_search_results_tab), excludeSelectedSession = !fromHomeFragment, suggestionsHeader = activity.getString(R.string.firefox_suggest_header), - resultsHostFilter = searchEngineHostFilter, + resultsUriFilter = searchEngineUriFilter, ) } diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt index 7c6209f49..dc6119ce8 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/SearchSelectorToolbarAction.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.ViewGroup import androidx.annotation.VisibleForTesting import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -21,7 +22,6 @@ import mozilla.components.lib.state.ext.flow import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.view.toScope -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.UnifiedSearch import org.mozilla.fenix.R @@ -87,7 +87,7 @@ class SearchSelectorToolbarAction( store.flow() .map { state -> state.searchEngineSource.searchEngine } .filterNotNull() - .ifChanged() + .distinctUntilChanged() .collect { searchEngine -> view.setIcon( icon = searchEngine.getScaledIcon(view.context).apply { diff --git a/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt b/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt index b8230946c..251b029ac 100644 --- a/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt +++ b/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt @@ -9,7 +9,9 @@ import android.app.ActivityManager import android.app.Application import android.content.Context import android.os.Bundle +import mozilla.components.browser.state.selector.privateTabs import org.mozilla.fenix.FenixApplication +import org.mozilla.fenix.ext.components /** * This ActivityLifecycleCallbacks implementations tracks if there is at least one activity in the @@ -50,6 +52,9 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?) override fun onActivityStopped(activity: Activity) { activitiesInStartedState-- + if (activitiesInStartedState == 0) { + removeTCPException(activity) + } } override fun onActivityResumed(activity: Activity) {} @@ -62,6 +67,18 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?) override fun onActivityDestroyed(activity: Activity) {} + /** + * For private tabs, set the tracking protection exception + * to the default state if the app is closed. + */ + private fun removeTCPException(activity: Activity) { + activity.components.core.store.state.privateTabs.filter { + it.trackingProtection.ignoredOnTrackingProtection + }.forEach { privateTab -> + activity.components.useCases.trackingProtectionUseCases.removeException(privateTab.id) + } + } + companion object { /** * If all activities of this app are in the background then finish and remove all tasks. After diff --git a/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt index a89feaac9..158201ce3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt @@ -33,11 +33,6 @@ class DataChoicesFragment : PreferenceFragmentCompat() { context.components.analytics.metrics.start(MetricServiceType.Data) } else { context.components.analytics.metrics.stop(MetricServiceType.Data) - - // Reset the Shared Prefs UUID on opt-out since we're investigating cases of - // unexpected client ID regeneration. Telemetry data collection opt-out is - // expected to reset the client ID. - context.settings().sharedPrefsUUID = "" } // Reset experiment identifiers on both opt-in and opt-out; it's likely // that in future we will need to pass in the new telemetry client_id diff --git a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt index a00968324..69880a15d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DefaultBrowserPreference.kt @@ -9,8 +9,8 @@ import android.util.AttributeSet import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.google.android.material.switchmaterial.SwitchMaterial +import mozilla.components.support.utils.BrowsersCache import org.mozilla.fenix.R -import org.mozilla.fenix.utils.BrowsersCache class DefaultBrowserPreference @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/settings/OnSharedPreferenceChangeListener.kt b/app/src/main/java/org/mozilla/fenix/settings/OnSharedPreferenceChangeListener.kt index f57e9cb86..8c15aeb9b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/OnSharedPreferenceChangeListener.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/OnSharedPreferenceChangeListener.kt @@ -6,11 +6,12 @@ package org.mozilla.fenix.settings import android.content.SharedPreferences import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleOwner class OnSharedPreferenceChangeListener( private val sharedPreferences: SharedPreferences, - private val listener: (SharedPreferences, String) -> Unit, + private val listener: (SharedPreferences, String?) -> Unit, ) : SharedPreferences.OnSharedPreferenceChangeListener, DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { @@ -21,14 +22,17 @@ class OnSharedPreferenceChangeListener( sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String) { + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) { listener(sharedPreferences, key) } } +/** + * Registers a [OnSharedPreferenceChangeListener] as a [LifecycleObserver] for a preference. + */ fun SharedPreferences.registerOnSharedPreferenceChangeListener( owner: LifecycleOwner, - listener: (SharedPreferences, String) -> Unit, + listener: (SharedPreferences, String?) -> Unit, ) { owner.lifecycle.addObserver(OnSharedPreferenceChangeListener(this, listener)) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index 7d721a5cb..1d1e161b0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -38,7 +38,6 @@ import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.ktx.android.view.showKeyboard -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags @@ -63,6 +62,7 @@ import org.mozilla.fenix.perf.ProfilerViewModel import org.mozilla.fenix.settings.account.AccountUiView import org.mozilla.fenix.utils.Settings import kotlin.system.exitProcess +import org.mozilla.fenix.GleanMetrics.Settings as SettingsMetrics @Suppress("LargeClass", "TooManyFunctions") class SettingsFragment : PreferenceFragmentCompat() { @@ -255,6 +255,7 @@ class SettingsFragment : PreferenceFragmentCompat() { val directions: NavDirections? = when (preference.key) { resources.getString(R.string.pref_key_sign_in) -> { + SettingsMetrics.signIntoSync.add() SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment( entrypoint = FenixFxAEntryPoint.SettingsMenu, ) @@ -420,7 +421,7 @@ class SettingsFragment : PreferenceFragmentCompat() { binding.customAmoUser.setText(context.settings().overrideAmoUser) binding.customAmoUser.requestFocus() binding.customAmoUser.showKeyboard() - create().withCenterAlignedButtons() + create() }.show() null diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt index 99fa4ca65..e027124f9 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutFragment.kt @@ -31,7 +31,6 @@ import org.mozilla.fenix.settings.about.AboutItemType.PRIVACY_NOTICE import org.mozilla.fenix.settings.about.AboutItemType.RIGHTS import org.mozilla.fenix.settings.about.AboutItemType.SUPPORT import org.mozilla.fenix.settings.about.AboutItemType.WHATS_NEW -import org.mozilla.fenix.utils.Do import org.mozilla.fenix.whatsnew.WhatsNew import org.mozilla.geckoview.BuildConfig as GeckoViewBuildConfig @@ -189,7 +188,7 @@ class AboutFragment : Fragment(), AboutPageListener { } override fun onAboutItemClicked(item: AboutItem) { - Do exhaustive when (item) { + when (item) { is AboutItem.ExternalLink -> { when (item.type) { WHATS_NEW -> { diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt index b83c8248c..d9721705a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt @@ -13,7 +13,6 @@ import android.widget.ListView import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.databinding.FragmentAboutLibrariesBinding import org.mozilla.fenix.ext.showToolbar @@ -77,7 +76,7 @@ class AboutLibrariesFragment : Fragment(R.layout.fragment_about_libraries) { [name] : either the name of the library, or its artifact name. See https://github.com/google/play-services-plugins/tree/master/oss-licenses-plugin - */ + */ val licensesData = resources .openRawResource(R.raw.third_party_licenses) .readBytes() @@ -99,7 +98,6 @@ class AboutLibrariesFragment : Fragment(R.layout.fragment_about_libraries) { .setTitle(libraryItem.name) .setMessage(libraryItem.license) .create() - .withCenterAlignedButtons() dialog.show() val textView = dialog.findViewById(android.R.id.message)!! diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt index ed5dbdb87..4539374d3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountSettingsFragment.kt @@ -34,19 +34,21 @@ import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncStatusObserver import mozilla.components.service.fxa.sync.getLastSynced import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras +import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.secure import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.requirePreference @SuppressWarnings("TooManyFunctions", "LargeClass") @@ -124,6 +126,11 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { accountManager = requireComponents.backgroundServices.accountManager accountManager.register(accountStateObserver, this, true) + // Manage account - only available on Nightly while we work on bug 1840492. + val preferenceManageAccount = requirePreference(R.string.pref_key_sync_manage_account) + preferenceManageAccount.isVisible = Config.channel.isNightlyOrDebug + preferenceManageAccount.onPreferenceClickListener = getClickListenerForManageAccount() + // Sign out val preferenceSignOut = requirePreference(R.string.pref_key_sign_out) preferenceSignOut.onPreferenceClickListener = getClickListenerForSignOut() @@ -286,7 +293,7 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { ) startActivity(intent) } - create().withCenterAlignedButtons() + create() }.show().secure(activity) it.settings().incrementShowLoginsSecureWarningSyncCount() } @@ -362,6 +369,22 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { return true } + private fun getClickListenerForManageAccount(): Preference.OnPreferenceClickListener { + return Preference.OnPreferenceClickListener { + viewLifecycleOwner.lifecycleScope.launch(Main) { + context?.let { + var acct = accountManager.authenticatedAccount() + var url = acct?.getManageAccountURL(FenixFxAEntryPoint.SettingsMenu) + if (url != null) { + val intent = SupportUtils.createCustomTabIntent(it, url) + startActivity(intent) + } + } + } + true + } + } + private fun getClickListenerForSignOut(): Preference.OnPreferenceClickListener { return Preference.OnPreferenceClickListener { accountSettingsInteractor.onSignOut() diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncController.kt b/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncController.kt index e7c4323f5..b04373308 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/DefaultSyncController.kt @@ -12,7 +12,6 @@ import android.provider.Settings import android.text.SpannableString import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.settings.SupportUtils @@ -69,7 +68,7 @@ class DefaultSyncController( dialog.cancel() activity.startActivity(intent) } - create().withCenterAlignedButtons() + create() } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt index 18858e0ce..517e73f2d 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/address/view/AddressEditorView.kt @@ -16,7 +16,6 @@ import mozilla.components.concept.storage.Address import mozilla.components.concept.storage.UpdatableAddressFields import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.GleanMetrics.Addresses import org.mozilla.fenix.R import org.mozilla.fenix.databinding.FragmentAddressEditorBinding @@ -121,7 +120,7 @@ class AddressEditorView( interactor.onDeleteAddress(guid) Addresses.deleted.add() } - create().withCenterAlignedButtons() + create() }.show() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt index fd2f5c923..e387bc4ae 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/DefaultLocaleSettingsController.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.settings.advanced import android.app.Activity import android.content.Context +import android.os.Build import mozilla.components.browser.state.action.SearchAction import mozilla.components.browser.state.store.BrowserStore import mozilla.components.support.locale.LocaleManager @@ -40,7 +41,12 @@ class DefaultLocaleSettingsController( // Invalidate cached values to use the new locale FxNimbus.features.nimbusValidation.withCachedValue(null) activity.recreate() - activity.overridePendingTransition(0, 0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0) + } else { + @Suppress("DEPRECATION") + activity.overridePendingTransition(0, 0) + } } override fun handleDefaultLocaleSelected() { @@ -55,7 +61,12 @@ class DefaultLocaleSettingsController( // Invalidate cached values to use the default locale FxNimbus.features.nimbusValidation.withCachedValue(null) activity.recreate() - activity.overridePendingTransition(0, 0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + activity.overrideActivityTransition(Activity.OVERRIDE_TRANSITION_OPEN, 0, 0) + } else { + @Suppress("DEPRECATION") + activity.overridePendingTransition(0, 0) + } } override fun handleSearchQueryTyped(query: String) { diff --git a/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt index 382f33162..839666f98 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/autofill/AutofillSettingFragment.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.launch import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider @@ -292,7 +291,7 @@ class AutofillSettingFragment : BiometricPromptPreferenceFragment() { startActivity(intent) } - create().withCenterAlignedButtons() + create() }.show().secure(activity) context.settings().incrementSecureWarningCount() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt index 5e16987b8..b71f5a631 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.SecureFragment import org.mozilla.fenix.databinding.FragmentCreditCardEditorBinding @@ -156,7 +155,7 @@ class CreditCardEditorFragment : dialog.cancel() } setPositiveButton(R.string.credit_cards_delete_dialog_button, onPositiveClickListener) - create().withCenterAlignedButtons() + create() }.show() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index b0716a498..d31ff2a63 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -15,12 +15,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.databinding.FragmentDeleteBrowsingDataBinding @@ -99,7 +98,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da scope = requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> flow.map { state -> state.tabs.size } - .ifChanged() + .distinctUntilChanged() .collect { openTabs -> updateTabCount(openTabs) } } } @@ -151,7 +150,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da it.dismiss() deleteSelected() } - create().withCenterAlignedButtons() + create() }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt index dcb0fb44b..bd4800ba5 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/AddLoginFragment.kt @@ -24,6 +24,7 @@ import androidx.navigation.fragment.findNavController import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard +import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.R import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.databinding.FragmentAddLoginBinding @@ -305,8 +306,8 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { layout.errorIconDrawable = null } } - clearButton.isVisible = validUsername - clearButton.isEnabled = validUsername + clearButton.isVisible = currentValue.isNotEmpty() + clearButton.isEnabled = currentValue.isNotEmpty() setSaveButtonState() } @@ -374,6 +375,7 @@ class AddLoginFragment : Fragment(R.layout.fragment_add_login), MenuProvider { binding.usernameText.text.toString(), binding.passwordText.text.toString(), ) + Logins.saved.add() true } else -> false diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt index 9d6d8c2d0..59432f5ea 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import com.google.android.material.textfield.TextInputLayout import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.glean.private.NoExtras import mozilla.components.support.ktx.android.view.hideKeyboard @@ -155,6 +156,24 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { binding.usernameText.addTextChangedListener( object : TextWatcher { override fun afterTextChanged(u: Editable?) { + when { + u.toString().isEmpty() -> { + validUsername = false + binding.clearUsernameTextButton.isVisible = false + setLayoutError( + context?.getString(R.string.saved_login_username_required), + binding.inputLayoutUsername, + ) + } + + else -> { + validUsername = true + binding.inputLayoutUsername.error = null + binding.inputLayoutUsername.errorIconDrawable = null + binding.inputLayoutUsername.isVisible = true + binding.clearUsernameTextButton.isVisible = true + } + } updateUsernameField() findDuplicate() setSaveButtonState() @@ -180,10 +199,14 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { override fun afterTextChanged(p: Editable?) { when { p.toString().isEmpty() -> { + validPassword = false passwordChanged = true binding.revealPasswordButton.isVisible = false binding.clearPasswordTextButton.isVisible = false - setPasswordError() + setLayoutError( + context?.getString(R.string.saved_login_password_required), + binding.inputLayoutPassword, + ) } p.toString() == oldLogin.password -> { passwordChanged = false @@ -239,10 +262,6 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { // existing login was already a dupe and the username hasn't // changed usernameChanged = oldLogin.username != currentValue - validUsername = true - layout.error = null - layout.errorIconDrawable = null - clearButton.isVisible = true } else -> { // Invalid login because it's a dupe of another one @@ -265,10 +284,9 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { setSaveButtonState() } - private fun setPasswordError() { - binding.inputLayoutPassword.let { layout -> - validPassword = false - layout.error = context?.getString(R.string.saved_login_password_required) + private fun setLayoutError(error: String?, inputLayout: TextInputLayout) { + inputLayout.let { layout -> + layout.error = error layout.setErrorIconDrawable(R.drawable.mozac_ic_warning_with_bottom_padding) layout.setErrorIconTintList( ColorStateList.valueOf( @@ -286,7 +304,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { inflater.inflate(R.menu.login_save, menu) } - override fun onPrepareOptionsMenu(menu: Menu) { + override fun onPrepareMenu(menu: Menu) { val saveButton = menu.findItem(R.id.save_login_button) val changesMadeWithNoErrors = validUsername && validPassword && (usernameChanged || passwordChanged) @@ -312,6 +330,7 @@ class EditLoginFragment : Fragment(R.layout.fragment_edit_login), MenuProvider { binding.passwordText.text.toString(), ) Logins.saveEditedLogin.record(NoExtras()) + Logins.modified.add() true } else -> false diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt index 7c60c6592..ad9994b8f 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt @@ -23,7 +23,6 @@ import androidx.navigation.fragment.navArgs import com.google.android.material.snackbar.Snackbar import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.service.glean.private.NoExtras -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.HomeActivity @@ -217,10 +216,11 @@ class LoginDetailFragment : SecureFragment(R.layout.fragment_login_detail), Menu } setPositiveButton(R.string.dialog_delete_positive) { dialog: DialogInterface, _ -> Logins.deleteSavedLogin.record(NoExtras()) + Logins.deleted.add() interactor.onDeleteLogin(args.savedLoginId) dialog.dismiss() } - create().withCenterAlignedButtons() + create() }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt index 1dc5496a6..58acc1c73 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt @@ -26,7 +26,6 @@ import mozilla.components.feature.autofill.preference.AutofillPreference import mozilla.components.service.fxa.SyncEngine 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.Logins import org.mozilla.fenix.R import org.mozilla.fenix.components.accounts.FenixFxAEntryPoint @@ -216,7 +215,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { val intent = Intent(ACTION_SECURITY_SETTINGS) startActivity(intent) } - create().withCenterAlignedButtons() + create() }.show().secure(activity) context.settings().incrementSecureWarningCount() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt index 7f9e3e7ff..bd123de81 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/ClearSiteDataView.kt @@ -18,7 +18,6 @@ import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.databinding.QuicksettingsClearSiteDataBinding import org.mozilla.fenix.ext.components @@ -108,7 +107,7 @@ class ClearSiteDataView( it.dismiss() interactor.onClearSiteDataClicked(baseDomain) } - create().withCenterAlignedButtons() + create() }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index 45103f21e..671600eb4 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -120,6 +120,7 @@ class DefaultQuickSettingsController( private val displayPermissions: () -> Unit, private val engine: Engine = context.components.core.engine, ) : QuickSettingsController { + override fun handlePermissionsShown() { displayPermissions() } @@ -170,7 +171,7 @@ class DefaultQuickSettingsController( } val sitePermissions = autoplayValue.createSitePermissionsFromCustomRules(origin, settings) - handleAutoplayAdd(sitePermissions) + handleAutoplayAdd(sitePermissions, tab?.content?.private ?: false) sitePermissions } else { val newPermission = autoplayValue.updateSitePermissions(permissions) @@ -298,9 +299,9 @@ class DefaultQuickSettingsController( } @VisibleForTesting - internal fun handleAutoplayAdd(sitePermissions: SitePermissions) { + internal fun handleAutoplayAdd(sitePermissions: SitePermissions, private: Boolean) { ioScope.launch { - permissionStorage.add(sitePermissions) + permissionStorage.add(sitePermissions, private) reload(sessionId) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt index fd156f963..16378f081 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/protections/cookiebanners/dialog/CookieBannerReEngagementDialog.kt @@ -107,6 +107,6 @@ class CookieBannerReEngagementDialog : DialogFragment() { } companion object { - private const val LENGTH_SNACKBAR_DURATION = 4000 /* 4 seconds in ms */ + private const val LENGTH_SNACKBAR_DURATION = 4000 // 4 seconds in ms } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt index 87d9b29e7..f0ddb4172 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt @@ -20,6 +20,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.MainScope +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import mozilla.components.browser.state.search.SearchEngine @@ -29,7 +30,6 @@ import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.ext.flow import mozilla.components.support.ktx.android.view.toScope -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.R import org.mozilla.fenix.databinding.SearchEngineRadioButtonBinding @@ -64,7 +64,7 @@ class RadioSearchEngineListPreference @JvmOverloads constructor( private fun subscribeToSearchEngineUpdates(store: BrowserStore, view: View) = view.toScope().launch { store.flow() .map { state -> state.search } - .ifChanged() + .distinctUntilChanged() .collect { state -> refreshSearchEngineViews(view, state) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt index addf2d85d..9b7dbc997 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.support.ktx.kotlin.stripDefaultPort -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents @@ -146,7 +145,7 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { setNegativeButton(R.string.clear_permissions_negative) { dialog: DialogInterface, _ -> dialog.cancel() } - }.show().withCenterAlignedButtons() + }.show() true } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsExceptionsFragment.kt index 613f86e07..99388b9fe 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsExceptionsFragment.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.support.ktx.kotlin.stripDefaultPort -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -120,7 +119,7 @@ class SitePermissionsExceptionsFragment : setNegativeButton(R.string.clear_permissions_negative) { dialog: DialogInterface, _ -> dialog.cancel() } - }.show().withCenterAlignedButtons() + }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt index f7c35ecf9..739b7eb62 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt @@ -26,7 +26,6 @@ import kotlinx.coroutines.launch import mozilla.components.concept.engine.permission.SitePermissions import mozilla.components.concept.engine.permission.SitePermissions.Status.ALLOWED import mozilla.components.concept.engine.permission.SitePermissions.Status.BLOCKED -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.R import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -163,7 +162,7 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { setNegativeButton(R.string.clear_permission_negative) { dialog: DialogInterface, _ -> dialog.cancel() } - }.show().withCenterAlignedButtons() + }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt index 7a82c9a88..39b87b0fd 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesAdapter.kt @@ -19,7 +19,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import com.google.android.material.button.MaterialButton import mozilla.components.service.nimbus.messaging.MESSAGING_FEATURE_ID -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.experiments.nimbus.internal.EnrolledExperiment import org.mozilla.fenix.R import org.mozilla.fenix.settings.studies.CustomViewHolder.SectionViewHolder @@ -142,7 +141,7 @@ class StudiesAdapter( .setTitle(R.string.preference_experiments_2) .setMessage(R.string.studies_restart_app) .setCancelable(false) - val alertDialog: AlertDialog = builder.create().withCenterAlignedButtons() + val alertDialog: AlertDialog = builder.create() alertDialog.show() return alertDialog } diff --git a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesView.kt b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesView.kt index f9ee948f8..077e618e8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/studies/StudiesView.kt @@ -22,7 +22,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.service.nimbus.NimbusApi import mozilla.components.support.base.log.logger.Logger -import mozilla.components.ui.widgets.withCenterAlignedButtons import mozilla.telemetry.glean.private.NoExtras import org.mozilla.experiments.nimbus.internal.EnrolledExperiment import org.mozilla.fenix.GleanMetrics.Preferences @@ -83,7 +82,7 @@ class StudiesView( .setTitle(R.string.preference_experiments_2) .setMessage(R.string.studies_restart_app) .setCancelable(false) - val alertDialog: AlertDialog = builder.create().withCenterAlignedButtons() + val alertDialog: AlertDialog = builder.create() alertDialog.show() } bindDescription() diff --git a/app/src/main/java/org/mozilla/fenix/share/AddNewDeviceFragment.kt b/app/src/main/java/org/mozilla/fenix/share/AddNewDeviceFragment.kt index 3c5150c32..1172ad883 100644 --- a/app/src/main/java/org/mozilla/fenix/share/AddNewDeviceFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/share/AddNewDeviceFragment.kt @@ -8,7 +8,6 @@ import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment -import mozilla.components.ui.widgets.withCenterAlignedButtons import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -45,7 +44,7 @@ class AddNewDeviceFragment : Fragment(R.layout.fragment_add_new_device) { AlertDialog.Builder(requireContext()).apply { setMessage(R.string.sync_connect_device_dialog) setPositiveButton(R.string.sync_confirmation_button) { dialog, _ -> dialog.cancel() } - create().withCenterAlignedButtons() + create() }.show() } } diff --git a/app/src/main/java/org/mozilla/fenix/share/PrintItem.kt b/app/src/main/java/org/mozilla/fenix/share/PrintItem.kt new file mode 100644 index 000000000..c38571589 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/PrintItem.kt @@ -0,0 +1,69 @@ +/* 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.share + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +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 Print item. + * + * @param onClick event handler when the print item is clicked. + */ +@Composable +fun PrintItem( + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .height(56.dp) + .fillMaxWidth() + .clickable(onClick = onClick), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(16.dp)) + + Icon( + painter = painterResource(R.drawable.ic_print), + contentDescription = stringResource( + R.string.content_description_close_button, + ), + tint = FirefoxTheme.colors.iconPrimary, + ) + + Spacer(Modifier.width(32.dp)) + + Text( + color = FirefoxTheme.colors.textPrimary, + text = stringResource(R.string.menu_print), + style = FirefoxTheme.typography.subtitle1, + ) + } +} + +@Composable +@Preview +@LightDarkPreview +private fun PrintItemPreview() { + FirefoxTheme { + PrintItem {} + } +} diff --git a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFInteractor.kt b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFInteractor.kt index 5bfcb7516..bf38574be 100644 --- a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFInteractor.kt @@ -13,4 +13,10 @@ interface SaveToPDFInteractor { * @param tabId The ID of the tab to save as PDF. */ fun onSaveToPDF(tabId: String?) + + /** + * Prints from the given [tabId]. + * @param tabId The ID of the tab to print. + */ + fun onPrint(tabId: String?) } diff --git a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt index 3b56847db..e8ae89af9 100644 --- a/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/share/SaveToPDFMiddleware.kt @@ -7,16 +7,30 @@ package org.mozilla.fenix.share import android.content.Context import android.widget.Toast import android.widget.Toast.LENGTH_LONG +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.state.Action import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext -import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.R import org.mozilla.gecko.util.ThreadUtils +import org.mozilla.geckoview.GeckoSession +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_NO_ACTIVITY_CONTEXT +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_NO_ACTIVITY_CONTEXT_DELEGATE +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_NO_PRINT_DELEGATE +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_UNABLE_TO_CREATE_PRINT_SETTINGS +import org.mozilla.geckoview.GeckoSession.GeckoPrintException.ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT +import java.io.IOException +import java.lang.Exception /** * [BrowserAction] middleware reacting in response to Save to PDF related [Action]s. @@ -24,6 +38,7 @@ import org.mozilla.gecko.util.ThreadUtils */ class SaveToPDFMiddleware( private val context: Context, + private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main), ) : Middleware { override fun invoke( @@ -31,15 +46,168 @@ class SaveToPDFMiddleware( next: (BrowserAction) -> Unit, action: BrowserAction, ) { - if (action is EngineAction.SaveToPdfExceptionAction) { - // See https://github.com/mozilla-mobile/fenix/issues/27649 for more details, - // why a Toast is used here. - ThreadUtils.runOnUiThread { - Toast.makeText(context, R.string.unable_to_save_to_pdf_error, LENGTH_LONG).show() - } - Events.saveToPdfFailure.record(NoExtras()) - } else { - next(action) + when (action) { + is EngineAction.SaveToPdfAction -> { + postTelemetryTapped(ctx.state.findTab(action.tabId)) + // Continue to generate the PDF, passing through here to add telemetry + next(action) + } + + is EngineAction.SaveToPdfCompleteAction -> { + postTelemetryCompleted(ctx.state.findTab(action.tabId)) + } + + is EngineAction.SaveToPdfExceptionAction -> { + // See https://github.com/mozilla-mobile/fenix/issues/27649 for more details, + // why a Toast is used here. + ThreadUtils.runOnUiThread { + Toast.makeText(context, R.string.unable_to_save_to_pdf_error, LENGTH_LONG).show() + } + + postTelemetryFailed(ctx.state.findTab(action.tabId), action.throwable) + } + + is EngineAction.PrintContentAction -> { + next(action) + // Reserved for telemetry in bug 1837517 + } + is EngineAction.PrintContentCompletedAction -> { + // No-op, reserved for telemetry in bug 1837517 + } + is EngineAction.PrintContentExceptionAction -> { + // Bug 1840894 - will update this toast to a snackbar with new snackbar error component + ThreadUtils.runOnUiThread { + Toast.makeText(context, R.string.unable_to_print_error, LENGTH_LONG).show() + } + } + else -> { + next(action) + } + } + } + + /** + * Use to generate failure extra reasons for Save To PDF failure telemetry. + * + * @param exception - A given exception that will be properly labeled for telemetry posting. + * @return processed failure reason to send in telemetry. + */ + @VisibleForTesting // package + fun telemetryErrorReason(exception: Exception): String { + var failureMsg = "unknown" + // Requiring information from GeckoView isn't a good practice, + // follow-up to improve this is bug 1838719 + if (exception is GeckoSession.GeckoPrintException) { + when (exception.code) { + ERROR_PRINT_SETTINGS_SERVICE_NOT_AVAILABLE -> failureMsg = "no_settings_service" + ERROR_UNABLE_TO_CREATE_PRINT_SETTINGS -> failureMsg = "no_settings" + ERROR_UNABLE_TO_RETRIEVE_CANONICAL_BROWSING_CONTEXT -> failureMsg = "no_canonical_context" + ERROR_NO_ACTIVITY_CONTEXT_DELEGATE -> failureMsg = "no_activity_context_delegate" + ERROR_NO_ACTIVITY_CONTEXT -> failureMsg = "no_activity_context" + ERROR_NO_PRINT_DELEGATE -> failureMsg = "no_print_delegate" + } + } + if (exception is IOException) { + failureMsg = "io_error" + } + return failureMsg + } + + /** + * Use to generate extra sources for Save To PDF telemetry. + * + * @param isPdfViewer - If the page has a PDF viewer or not. + * @return processed page source type to send in telemetry. + */ + @VisibleForTesting // package + fun telemetrySource(isPdfViewer: Boolean?): String { + val source = when (isPdfViewer) { + null -> "unknown" + true -> "pdf" + false -> "non-pdf" + } + return source + } + + /** + * Indicates the Save As PDF action was requested and posts telemetry via Glean. + * + * @param tab - tab state to use for page source category + */ + private fun postTelemetryTapped(tab: TabSessionState?) { + mainScope.launch { + tab?.engineState?.engineSession?.checkForPdfViewer( + onResult = { isPdf -> + Events.saveToPdfTapped.record( + Events.SaveToPdfTappedExtra( + source = telemetrySource(isPdf), + ), + ) + }, + onException = { + Events.saveToPdfTapped.record( + Events.SaveToPdfTappedExtra( + source = telemetrySource(null), + ), + ) + }, + ) + } + } + + /** + * Indicates the Save As PDF action completed and generated a PDF and posts telemetry via Glean. + * + * @param tab - tab state to use for page source category + */ + private fun postTelemetryCompleted(tab: TabSessionState?) { + mainScope.launch { + tab?.engineState?.engineSession?.checkForPdfViewer( + onResult = { isPdf -> + Events.saveToPdfCompleted.record( + Events.SaveToPdfCompletedExtra( + source = telemetrySource(isPdf), + ), + ) + }, + onException = { + Events.saveToPdfCompleted.record( + Events.SaveToPdfCompletedExtra( + source = telemetrySource(null), + ), + ) + }, + ) + } + } + + /** + * Indicates the Save As PDF action failed and the reason for failure and posts telemetry via Glean. + * + * @param tab - tab state to use for page source category + * @param throwable - failure state to use for failure reason category + */ + private fun postTelemetryFailed(tab: TabSessionState?, throwable: Throwable) { + val telFailureReason = telemetryErrorReason(throwable as Exception) + mainScope.launch { + tab?.engineState?.engineSession?.checkForPdfViewer( + onResult = { isPdf -> + Events.saveToPdfFailure.record( + Events.SaveToPdfFailureExtra( + source = telemetrySource(isPdf), + reason = telFailureReason, + ), + ) + }, + onException = { + Events.saveToPdfFailure.record( + Events.SaveToPdfFailureExtra( + source = telemetrySource(null), + reason = telFailureReason, + ), + ) + }, + ) } } } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index 4ae4ece60..c43b55599 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -61,6 +61,11 @@ interface ShareController { fun handleShareToAllDevices(devices: List) fun handleSignIn() + /** + * Handles when a print action was requested. + */ + fun handlePrint(tabId: String?) + enum class Result { DISMISSED, SHARE_ERROR, SUCCESS } @@ -85,6 +90,7 @@ class DefaultShareController( private val shareData: List, private val sendTabUseCases: SendTabUseCases, private val saveToPdfUseCase: SessionUseCases.SaveToPdfUseCase, + private val printUseCase: SessionUseCases.PrintContentUseCase, private val snackbar: FenixSnackbar, private val navController: NavController, private val recentAppsStorage: RecentAppsStorage, @@ -146,11 +152,15 @@ class DefaultShareController( } override fun handleSaveToPDF(tabId: String?) { - Events.saveToPdfTapped.record(NoExtras()) handleShareClosed() saveToPdfUseCase.invoke(tabId) } + override fun handlePrint(tabId: String?) { + handleShareClosed() + printUseCase.invoke(tabId) + } + override fun handleAddNewDevice() { val directions = ShareFragmentDirections.actionShareFragmentToAddNewDeviceFragment() navController.navigate(directions) diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt index 30a34c4b7..5a66baf7f 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt @@ -10,7 +10,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatDialogFragment -import androidx.core.view.isVisible import androidx.fragment.app.clearFragmentResult import androidx.fragment.app.setFragmentResult import androidx.fragment.app.viewModels @@ -20,6 +19,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.engine.prompt.PromptRequest import mozilla.components.feature.accounts.push.SendTabUseCases import mozilla.components.feature.share.RecentAppsStorage @@ -27,6 +27,7 @@ import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.databinding.FragmentShareBinding +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.theme.FirefoxTheme @@ -50,11 +51,17 @@ class ShareFragment : AppCompatDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("ShareFragment onCreate"), + ) setStyle(STYLE_NO_TITLE, R.style.ShareDialogStyle) } override fun onPause() { super.onPause() + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("ShareFragment dismiss"), + ) consumePrompt { onDismiss() } dismiss() } @@ -85,6 +92,7 @@ class ShareFragment : AppCompatDialogFragment() { navController = findNavController(), sendTabUseCases = SendTabUseCases(accountManager), saveToPdfUseCase = requireComponents.useCases.sessionUseCases.saveToPdf, + printUseCase = requireComponents.useCases.sessionUseCases.printContent, recentAppsStorage = RecentAppsStorage(requireContext()), viewLifecycleScope = viewLifecycleOwner.lifecycleScope, ) { result -> @@ -116,15 +124,19 @@ class ShareFragment : AppCompatDialogFragment() { } shareToAppsView = ShareToAppsView(binding.appsShareLayout, shareInteractor) - if (FeatureFlags.saveToPDF) { - binding.dividerLineAppsShareAndPdfSection.isVisible = true - binding.savePdf.apply { - isVisible = true - setContent { - FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { - SaveToPDFItem { - shareInteractor.onSaveToPDF(tabId = args.sessionId) - } + binding.savePdf.setContent { + FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { + SaveToPDFItem { + shareInteractor.onSaveToPDF(tabId = args.sessionId) + } + } + } + + if (FeatureFlags.print) { + binding.print.setContent { + FirefoxTheme(theme = Theme.getTheme(allowPrivateTheme = false)) { + PrintItem { + shareInteractor.onPrint(tabId = args.sessionId) } } } @@ -146,6 +158,9 @@ class ShareFragment : AppCompatDialogFragment() { } override fun onDestroy() { + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("ShareFragment onDestroy"), + ) setFragmentResult(RESULT_KEY, Bundle()) // Clear the stored result in case there is no listener with the same key set. clearFragmentResult(RESULT_KEY) diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt b/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt index bd945b5ce..28c5e9cef 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt @@ -12,7 +12,10 @@ import org.mozilla.fenix.share.listadapters.AppShareOption */ class ShareInteractor( private val controller: ShareController, -) : ShareCloseInteractor, ShareToAccountDevicesInteractor, ShareToAppsInteractor, SaveToPDFInteractor { +) : ShareCloseInteractor, + ShareToAccountDevicesInteractor, + ShareToAppsInteractor, + SaveToPDFInteractor { override fun onReauth() { controller.handleReauth() } @@ -44,4 +47,8 @@ class ShareInteractor( override fun onSaveToPDF(tabId: String?) { controller.handleSaveToPDF(tabId) } + + override fun onPrint(tabId: String?) { + controller.handlePrint(tabId) + } } diff --git a/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt b/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt index bd4a343c5..56ddbd526 100644 --- a/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/share/viewholders/AccountDeviceViewHolder.kt @@ -14,7 +14,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.databinding.AccountShareListItemBinding import org.mozilla.fenix.share.ShareToAccountDevicesInteractor import org.mozilla.fenix.share.listadapters.SyncShareOption -import org.mozilla.fenix.utils.Do class AccountDeviceViewHolder( itemView: View, @@ -31,7 +30,7 @@ class AccountDeviceViewHolder( private fun bindClickListeners(option: SyncShareOption) { itemView.setOnClickListener { - Do exhaustive when (option) { + when (option) { SyncShareOption.SignIn -> interactor.onSignIn() SyncShareOption.AddNewDevice -> interactor.onAddNewDevice() is SyncShareOption.SendAll -> interactor.onShareToAllDevices(option.devices) diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt new file mode 100644 index 000000000..2a1e154ca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFeature.kt @@ -0,0 +1,31 @@ +/* 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.shopping + +import mozilla.components.support.base.feature.LifecycleAwareFeature +import org.mozilla.fenix.nimbus.FxNimbus + +/** + * Feature implementation that provides review quality check information for supported product + * pages. + * + * @param onAvailabilityChange Invoked when availability of this feature changes based on feature + * flag and when the loaded page is a supported product page. + */ +class ReviewQualityCheckFeature( + private val onAvailabilityChange: (isAvailable: Boolean) -> Unit, +) : LifecycleAwareFeature { + + override fun start() { + val isFeatureEnabled = FxNimbus.features.shoppingExperience.value().enabled + // Update to use product page detector api in Bug 1840580 + val isSupportedProductPage = false + onAvailabilityChange(isFeatureEnabled && isSupportedProductPage) + } + + override fun stop() { + // no-op + } +} diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt new file mode 100644 index 000000000..18714750c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ReviewQualityCheckFragment.kt @@ -0,0 +1,54 @@ +/* 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.shopping + +import android.app.Dialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.mozilla.fenix.shopping.ui.ReviewQualityCheckContent +import org.mozilla.fenix.theme.FirefoxTheme + +/** + * A bottom sheet fragment displaying Review Quality Check information. + */ +class ReviewQualityCheckFragment : BottomSheetDialogFragment() { + + private var behavior: BottomSheetBehavior? = null + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + super.onCreateDialog(savedInstanceState).apply { + setOnShowListener { + val bottomSheet = + findViewById(com.google.android.material.R.id.design_bottom_sheet) + bottomSheet?.setBackgroundResource(android.R.color.transparent) + behavior = BottomSheetBehavior.from(bottomSheet) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View = ComposeView(requireContext()).apply { + setContent { + FirefoxTheme { + ReviewQualityCheckContent( + onRequestDismiss = { + behavior?.state = BottomSheetBehavior.STATE_HIDDEN + }, + modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()), + ) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt new file mode 100644 index 000000000..78663ad03 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/shopping/ui/ReviewQualityCheckContent.kt @@ -0,0 +1,107 @@ +/* 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.shopping.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp +import org.mozilla.fenix.R +import org.mozilla.fenix.compose.BottomSheetHandle +import org.mozilla.fenix.compose.annotation.LightDarkPreview +import org.mozilla.fenix.theme.FirefoxTheme + +private val bottomSheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) +private const val BOTTOM_SHEET_HANDLE_WIDTH_PERCENT = 0.1f + +/** + * Top-level UI for the Review Quality Check feature. + * + * @param onRequestDismiss Invoked when a user actions requests dismissal of the bottom sheet. + * @param modifier The modifier to be applied to the Composable. + */ +@Composable +fun ReviewQualityCheckContent( + onRequestDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background( + color = FirefoxTheme.colors.layer1, + shape = bottomSheetShape, + ) + .padding(16.dp), + ) { + BottomSheetHandle( + onRequestDismiss = onRequestDismiss, + contentDescription = stringResource(R.string.browser_menu_review_quality_check_close), + modifier = Modifier + .fillMaxWidth(BOTTOM_SHEET_HANDLE_WIDTH_PERCENT) + .align(Alignment.CenterHorizontally), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Header() + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Composable +private fun Header() { + Row( + modifier = Modifier.semantics(mergeDescendants = true) {}, + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ic_firefox), + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + + Spacer(modifier = Modifier.width(10.dp)) + + Text( + text = stringResource(R.string.review_quality_check), + color = FirefoxTheme.colors.textPrimary, + style = FirefoxTheme.typography.headline6, + ) + } +} + +@Composable +@LightDarkPreview +private fun ReviewQualityCheckContentPreview() { + FirefoxTheme { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomCenter, + ) { + ReviewQualityCheckContent( + onRequestDismiss = {}, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt index e47145de5..0e1544c5b 100644 --- a/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/shortcut/PwaOnboardingObserver.kt @@ -9,13 +9,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.mapNotNull import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.pwa.WebAppUseCases import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.ext.nav @@ -39,7 +38,7 @@ class PwaOnboardingObserver( flow.mapNotNull { state -> state.selectedTab } - .ifChanged { + .distinctUntilChangedBy { it.content.webAppManifest } .collect { diff --git a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt index 61bdb77b4..8a0292ea4 100644 --- a/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabhistory/TabHistoryDialogFragment.kt @@ -12,12 +12,11 @@ import androidx.navigation.fragment.findNavController import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.mapNotNull import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.databinding.FragmentTabHistoryDialogBinding import org.mozilla.fenix.ext.requireComponents @@ -54,7 +53,7 @@ class TabHistoryDialogFragment : BottomSheetDialogFragment() { requireComponents.core.store.flowScoped(viewLifecycleOwner) { flow -> flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(customTabSessionId)?.content?.history } - .ifChanged() + .distinctUntilChanged() .collect { historyState -> tabHistoryView.updateState(historyState) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt index 368eb58b9..b434c1e5e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt @@ -6,7 +6,7 @@ package org.mozilla.fenix.tabstray import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.normalTabs @@ -14,7 +14,6 @@ import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged /** * A binding that closes the tabs tray when the last tab is closed. @@ -29,7 +28,7 @@ class CloseOnLastTabBinding( flow.map { it } // Ignore the initial state; we don't want to close immediately. .drop(1) - .ifChanged { it.tabs } + .distinctUntilChangedBy { it.tabs } .collect { state -> val selectedPage = tabsTrayStore.state.selectedPage val tabs = when (selectedPage) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt index c3272a053..7a352b6e9 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt @@ -9,7 +9,6 @@ import androidx.annotation.VisibleForTesting import com.google.android.material.tabs.TabLayout import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.state.store.BrowserStore -import org.mozilla.fenix.utils.Do /** * A wrapper class that building the tabs tray menu that handles item clicks. @@ -40,7 +39,7 @@ class MenuIntegration( @VisibleForTesting internal fun handleMenuClicked(item: TabsTrayMenu.Item) { - Do exhaustive when (item) { + when (item) { is TabsTrayMenu.Item.ShareAllTabs -> navigationInteractor.onShareTabsOfTypeClicked(isPrivateMode) is TabsTrayMenu.Item.OpenAccountSettings -> diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt index 8f5013892..2e60e5676 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt @@ -6,13 +6,12 @@ package org.mozilla.fenix.tabstray import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.ui.tabcounter.TabCounter /** @@ -26,7 +25,7 @@ class TabCounterBinding( override suspend fun onState(flow: Flow) { flow.map { it.normalTabs } - .ifChanged() + .distinctUntilChanged() .collect { counter.setCount(it.size) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt index 3163285bb..f6a2c6b30 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTray.kt @@ -167,6 +167,7 @@ fun TabsTray( pageCount = Page.values().size, modifier = Modifier.fillMaxSize(), state = pagerState, + beyondBoundsPageCount = 2, userScrollEnabled = false, ) { position -> when (Page.positionToPage(position)) { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt index 5f420f854..881602321 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayBanner.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton @@ -45,7 +44,6 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.lib.state.ext.observeAsComposableState @@ -57,6 +55,7 @@ import org.mozilla.fenix.compose.annotation.LightDarkPreview import org.mozilla.fenix.theme.FirefoxTheme private val ICON_SIZE = 24.dp +private const val MAX_WIDTH_TAB_ROW_PERCENT = 0.5f /** * Top-level UI for displaying the banner in [TabsTray]. @@ -168,7 +167,7 @@ private fun SingleSelectBanner( CompositionLocalProvider(LocalRippleTheme provides DisabledRippleTheme) { TabRow( selectedTabIndex = selectedPage.ordinal, - modifier = Modifier.width(180.dp), + modifier = Modifier.fillMaxWidth(MAX_WIDTH_TAB_ROW_PERCENT), backgroundColor = Color.Transparent, contentColor = selectedColor, divider = {}, @@ -428,10 +427,8 @@ private fun MultiSelectBanner( Text( text = stringResource(R.string.tab_tray_multi_select_title, selectedTabCount), - style = FirefoxTheme.typography.body1, + style = FirefoxTheme.typography.headline6, color = FirefoxTheme.colors.textOnColorPrimary, - fontSize = 20.sp, - fontWeight = FontWeight.W500, ) Spacer(modifier = Modifier.weight(1.0f)) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt index 6f53c9e9c..fdc2866e2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -28,7 +28,6 @@ import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Events -import org.mozilla.fenix.GleanMetrics.ServerKnobs import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R @@ -476,9 +475,6 @@ class DefaultTabsTrayController( TabsTray.newPrivateTabTapped.record(NoExtras()) } else { TabsTray.newTabTapped.record(NoExtras()) - - // Temporary recording for validating the Glean Server Knobs functionality. - ServerKnobs.validation.record(NoExtras()) } } @@ -536,7 +532,9 @@ class DefaultTabsTrayController( handleNavigateToBrowser() } tab.id in selected.map { it.id } -> handleTabUnselected(tab) - else -> tabsTrayStore.dispatch(TabsTrayAction.AddSelectTab(tab)) + source != TrayPagerAdapter.INACTIVE_TABS_FEATURE_NAME -> { + tabsTrayStore.dispatch(TabsTrayAction.AddSelectTab(tab)) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFab.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFab.kt index 66b619881..72af1abae 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFab.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFab.kt @@ -58,11 +58,11 @@ fun TabsTrayFab( Page.SyncedTabs -> { icon = painterResource(id = R.drawable.ic_fab_sync) - contentDescription = stringResource(id = R.string.tab_drawer_fab_sync) + contentDescription = stringResource(id = R.string.resync_button_content_description) label = if (isSyncing) { stringResource(id = R.string.sync_syncing_in_progress) } else { - stringResource(id = R.string.resync_button_content_description) + stringResource(id = R.string.tab_drawer_fab_sync) }.uppercase() onClick = onSyncedTabsFabClicked } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index e4ea11b4b..6c140519b 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -29,6 +29,7 @@ import mozilla.appservices.places.BookmarkRoot import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.feature.downloads.ui.DownloadCancelDialogFragment import mozilla.components.feature.tabs.tabstray.TabsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper @@ -125,6 +126,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment dismissTabsTray"), + ) setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle) } @@ -190,13 +194,18 @@ class TabsTrayFragment : AppCompatDialogFragment() { controller = tabsTrayController, ) + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment onCreateDialog"), + ) tabsTrayDialog = TabsTrayDialog(requireContext(), theme) { tabsTrayInteractor } return tabsTrayDialog } override fun onPause() { super.onPause() - + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment onPause"), + ) dialog?.window?.setWindowAnimations(R.style.DialogFragmentRestoreAnimation) } @@ -313,6 +322,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { override fun onStart() { super.onStart() + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment onStart"), + ) findPreviousDialogFragment()?.let { dialog -> dialog.onAcceptClicked = ::onCancelDownloadWarningAccepted } @@ -320,6 +332,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { override fun onDestroyView() { super.onDestroyView() + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment onDestroyView"), + ) _tabsTrayBinding = null _tabsTrayDialogBinding = null _fabButtonBinding = null @@ -534,12 +549,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) - if (!requireContext().settings().enableTabsTrayToCompose) { - trayBehaviorManager.updateDependingOnOrientation(newConfig.orientation) - - if (requireContext().settings().gridTabView) { - tabsTrayBinding.tabsTray.adapter?.notifyDataSetChanged() - } + trayBehaviorManager.updateDependingOnOrientation(newConfig.orientation) + if (!requireContext().settings().enableTabsTrayToCompose && requireContext().settings().gridTabView) { + tabsTrayBinding.tabsTray.adapter?.notifyDataSetChanged() } } @@ -554,6 +566,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { @VisibleForTesting internal fun showCancelledDownloadWarning(downloadCount: Int, tabId: String?, source: String?) { + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("DownloadCancelDialogFragment show"), + ) val dialog = DownloadCancelDialogFragment.newInstance( downloadCount = downloadCount, tabId = tabId, @@ -690,6 +705,9 @@ class TabsTrayFragment : AppCompatDialogFragment() { internal fun dismissTabsTray() { // This should always be the last thing we do because nothing (e.g. telemetry) // is guaranteed after that. + context?.components?.analytics?.crashReporter?.recordCrashBreadcrumb( + Breadcrumb("TabsTrayFragment dismissTabsTray"), + ) dismissAllowingStateLoss() } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInactiveTabsOnboardingBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInactiveTabsOnboardingBinding.kt index ec9bc5370..77b531408 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInactiveTabsOnboardingBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInactiveTabsOnboardingBinding.kt @@ -17,14 +17,13 @@ import android.view.View import androidx.annotation.VisibleForTesting import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.helpers.AbstractBinding import mozilla.components.support.ktx.android.util.dpToPx -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.R @@ -50,7 +49,7 @@ class TabsTrayInactiveTabsOnboardingBinding( override suspend fun onState(flow: Flow) { flow.map { state -> state.normalTabs.size } - .ifChanged() + .distinctUntilChanged() .collect { val inactiveTabsList = if (settings.inactiveTabsAreEnabled) { store.state.potentialInactiveTabs } else { emptyList() } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt index 5e181c530..efe834204 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt @@ -10,14 +10,13 @@ import android.view.ViewGroup import androidx.annotation.VisibleForTesting import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.browser.infobanner.InfoBanner import org.mozilla.fenix.utils.Settings @@ -37,7 +36,7 @@ class TabsTrayInfoBannerBinding( override suspend fun onState(flow: Flow) { flow.map { state -> max(state.normalTabs.size, state.privateTabs.size) } - .ifChanged() + .distinctUntilChanged() .collect { tabCount -> if (tabCount >= TAB_COUNT_SHOW_CFR) { displayInfoBannerIfNeeded(settings) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BlankDragShadowBuilder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BlankDragShadowBuilder.kt index 565da74bd..a437bc5e7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BlankDragShadowBuilder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BlankDragShadowBuilder.kt @@ -16,7 +16,7 @@ class BlankDragShadowBuilder : View.DragShadowBuilder() { outShadowTouchPoint?.y = 0 } - override fun onDrawShadow(canvas: Canvas?) { + override fun onDrawShadow(canvas: Canvas) { // Do nothing } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt index 2307ccbb6..464c2034c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/NormalTabsBinding.kt @@ -5,11 +5,10 @@ package org.mozilla.fenix.tabstray.browser import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChangedBy import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.tabstray.TabsTray import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore @@ -22,7 +21,7 @@ class NormalTabsBinding( private val tabsTray: TabsTray, ) : AbstractBinding(store) { override suspend fun onState(flow: Flow) { - flow.ifChanged { it.normalTabs } + flow.distinctUntilChangedBy { it.normalTabs } .collect { tabsTray.updateTabs(it.normalTabs, null, browserStore.state.selectedTabId) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt index 861b43aaf..03e1983e7 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/PrivateTabsBinding.kt @@ -5,12 +5,11 @@ package org.mozilla.fenix.tabstray.browser import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.tabstray.TabsTray import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore @@ -24,7 +23,7 @@ class PrivateTabsBinding( ) : AbstractBinding(store) { override suspend fun onState(flow: Flow) { flow.map { it.privateTabs } - .ifChanged() + .distinctUntilChanged() .collect { // Getting the selectedTabId from the BrowserStore at a different time might lead to a race. tray.updateTabs(it, null, browserStore.state.selectedTabId) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt index 1fb3fa44a..3e36d9447 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt @@ -7,12 +7,11 @@ package org.mozilla.fenix.tabstray.browser import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayState.Mode import org.mozilla.fenix.tabstray.TabsTrayStore @@ -28,7 +27,7 @@ class SelectedItemAdapterBinding( override suspend fun onState(flow: Flow) { flow.map { it.mode } - .ifChanged() + .distinctUntilChanged() .collect { mode -> notifyAdapter(mode) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt index da2ee01c8..9b4a7cbcc 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt @@ -11,9 +11,9 @@ import androidx.core.content.ContextCompat import androidx.core.view.isVisible import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.databinding.ComponentTabstray2Binding import org.mozilla.fenix.databinding.TabstrayMultiselectItemsBinding @@ -62,7 +62,7 @@ class SelectionBannerBinding( override suspend fun onState(flow: Flow) { flow.map { it.mode } - .ifChanged() + .distinctUntilChanged() .collect { mode -> val isSelectMode = mode is Select @@ -90,7 +90,9 @@ class SelectionBannerBinding( } tabsTrayMultiselectItemsBinding.collectMultiSelect.setOnClickListener { - interactor.onAddSelectedTabsToCollectionClicked() + if (store.state.mode.selectedTabs.isNotEmpty()) { + interactor.onAddSelectedTabsToCollectionClicked() + } } binding.exitMultiSelect.setOnClickListener { diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt index 39f451f6a..ee999934a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt @@ -12,10 +12,9 @@ import androidx.core.content.ContextCompat import androidx.core.view.updateLayoutParams import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayState.Mode @@ -42,7 +41,7 @@ class SelectionHandleBinding( override suspend fun onState(flow: Flow) { flow.map { it.mode } - .ifChanged() + .distinctUntilChanged() .collect { mode -> val isSelectMode = mode is Mode.Select diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt index d324dcf73..9c796915d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt @@ -8,7 +8,6 @@ import android.content.Context import androidx.annotation.VisibleForTesting import mozilla.components.browser.menu.BrowserMenuBuilder import org.mozilla.fenix.tabstray.TabsTrayInteractor -import org.mozilla.fenix.utils.Do class SelectionMenuIntegration( private val context: Context, @@ -25,7 +24,7 @@ class SelectionMenuIntegration( @VisibleForTesting internal fun handleMenuClicked(item: SelectionMenu.Item) { - Do exhaustive when (item) { + when (item) { is SelectionMenu.Item.BookmarkTabs -> { interactor.onBookmarkSelectedTabsClicked() } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt index f86e0ab04..281c7f4de 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt @@ -6,10 +6,9 @@ package org.mozilla.fenix.tabstray.browser import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore @@ -25,7 +24,7 @@ class SwipeToDeleteBinding( override suspend fun onState(flow: Flow) { flow.map { it.mode } - .ifChanged() + .distinctUntilChanged() .collect { mode -> isSwipeable = mode == TabsTrayState.Mode.Normal } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt b/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt index 26cdb12f3..8152b9e05 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/inactivetabs/InactiveTabs.kt @@ -29,6 +29,8 @@ import androidx.compose.runtime.remember 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.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -101,10 +103,15 @@ fun InactiveTabsList( Column { inactiveTabs.forEach { tab -> val tabUrl = tab.content.url.toShortUrl() + val faviconPainter = tab.content.icon?.run { + prepareToDraw() + BitmapPainter(asImageBitmap()) + } FaviconListItem( label = tab.toDisplayTitle(), description = tabUrl, + faviconPainter = faviconPainter, onClick = { onTabClick(tab) }, url = tabUrl, iconPainter = painterResource(R.drawable.mozac_ic_close), diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt index d25c1e1d6..0273c4069 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt @@ -6,11 +6,10 @@ package org.mozilla.fenix.tabstray.syncedtabs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.feature.syncedtabs.view.SyncedTabsView import mozilla.components.lib.state.helpers.AbstractBinding -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.tabstray.TabsTrayState import org.mozilla.fenix.tabstray.TabsTrayStore @@ -27,7 +26,7 @@ class SyncButtonBinding( ) : AbstractBinding(tabsTrayStore) { override suspend fun onState(flow: Flow) { flow.map { it.syncing } - .ifChanged() + .distinctUntilChanged() .collect { syncingNow -> if (syncingNow) { onSyncNow() diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt index a606a639b..ba5cb2c84 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserPageViewHolder.kt @@ -10,12 +10,12 @@ import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import mozilla.components.browser.state.selector.selectedNormalTab import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.AppStore import org.mozilla.fenix.components.appstate.AppAction @@ -129,7 +129,7 @@ class NormalBrowserPageViewHolder( private fun observeTabsTrayInactiveTabsState(adapter: RecyclerView.Adapter) { tabsTrayStore.flowScoped(lifecycleOwner) { flow -> flow.map { state -> state.inactiveTabs } - .ifChanged() + .distinctUntilChanged() .collect { inactiveTabs -> inactiveTabsSize = inactiveTabs.size updateTrayVisibility(showTrayList(adapter)) diff --git a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt index c179d0c34..f67c3ab05 100644 --- a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt @@ -13,6 +13,7 @@ import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.SessionState import mozilla.components.concept.base.crash.CrashReporting @@ -94,6 +95,7 @@ class TelemetryMiddleware( -> { // Update/Persist tabs count whenever it changes settings.openTabsCount = context.state.normalTabs.count() + settings.openPrivateTabsCount = context.state.privateTabs.count() if (context.state.normalTabs.isNotEmpty()) { Metrics.hasOpenTabs.set(true) } else { diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt index 33c4da6f9..31efd9547 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragment.kt @@ -24,6 +24,7 @@ import androidx.navigation.fragment.navArgs import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.plus @@ -36,7 +37,6 @@ import mozilla.components.lib.state.ext.observe import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.telemetry.glean.private.NoExtras import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.GleanMetrics.TrackingProtection @@ -221,7 +221,7 @@ class TrackingProtectionPanelDialogFragment : AppCompatDialogFragment(), UserInt consumeFlow(store) { flow -> flow.mapNotNull { state -> state.findTabOrCustomTab(provideCurrentTabId()) - }.ifChanged { tab -> tab.content.url } + }.distinctUntilChangedBy { tab -> tab.content.url } .collect { protectionsStore.dispatch(ProtectionsAction.UrlChange(it.content.url)) } diff --git a/app/src/main/java/org/mozilla/fenix/utils/BrowsersCache.kt b/app/src/main/java/org/mozilla/fenix/utils/BrowsersCache.kt deleted file mode 100644 index 391a9a0f0..000000000 --- a/app/src/main/java/org/mozilla/fenix/utils/BrowsersCache.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.utils - -import android.content.Context -import androidx.annotation.VisibleForTesting -import mozilla.components.support.utils.Browsers - -/** - * Caches the list of browsers installed on a user's device. - * - * BrowsersCache caches the list of installed browsers is gathered lazily when it is first accessed - * after initial creation or invalidation. For that reason, a context is required every time - * the cache is accessed. - * - * Users are responsible for invalidating the cache at the appropriate time. It is left up to the - * user to determine appropriate policies for maintaining the validity of the cache. If, when the - * cache is accessed, it is filled, the contents will be returned. As mentioned above, the cache - * will be lazily refilled after invalidation. In other words, invalidation is O(1). - * - * This cache is threadsafe. - */ -object BrowsersCache { - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal var cachedBrowsers: Browsers? = null - - @Synchronized - fun all(context: Context): Browsers { - run { - val cachedBrowsers = cachedBrowsers - if (cachedBrowsers != null) { - return cachedBrowsers - } - } - return Browsers.all(context).also { - this.cachedBrowsers = it - } - } - - @Synchronized - fun resetAll() { - cachedBrowsers = null - } -} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Do.kt b/app/src/main/java/org/mozilla/fenix/utils/Do.kt deleted file mode 100644 index 802c92302..000000000 --- a/app/src/main/java/org/mozilla/fenix/utils/Do.kt +++ /dev/null @@ -1,18 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.utils - -object Do { - - /** - * Indicates to the linter that the following when statement should be exhaustive. - * - * @sample Do exhaustive when (bool) { - * true -> Unit - * false -> Unit - * } - */ - inline infix fun exhaustive(any: T?) = any -} diff --git a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt index ac36fcbfe..47bb24cc1 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -29,6 +29,7 @@ import mozilla.components.support.ktx.android.content.longPreference import mozilla.components.support.ktx.android.content.stringPreference import mozilla.components.support.ktx.android.content.stringSetPreference import mozilla.components.support.locale.LocaleManager +import mozilla.components.support.utils.BrowsersCache import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config import org.mozilla.fenix.FeatureFlags @@ -185,6 +186,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = "", ) + var nimbusExperimentsFetched by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_nimbus_experiments_fetched), + default = false, + ) + var utmParamsKnown by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_utm_params_known), default = false, @@ -220,16 +226,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = "", ) - /** - * A UUID stored in Shared Preferences used to analyze technical differences - * between storage mechanisms in Android, specifically the Glean DB and - * Shared Preferences. - */ - var sharedPrefsUUID by stringPreference( - appContext.getPreferenceKey(R.string.pref_key_shared_prefs_uuid), - default = "", - ) - var currentWallpaperName by stringPreference( appContext.getPreferenceKey(R.string.pref_key_current_wallpaper), default = Wallpaper.Default.name, @@ -444,6 +440,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = true, ) + var isFirstSplashScreenShown: Boolean by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_is_first_splash_screen_shown), + default = false, + ) + var nimbusLastFetchTime: Long by longPreference( appContext.getPreferenceKey(R.string.pref_key_nimbus_last_fetch), default = 0L, @@ -1363,6 +1364,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { 0, ) + var openPrivateTabsCount by intPreference( + appContext.getPreferenceKey(R.string.pref_key_open_private_tabs_count), + 0, + ) + var mobileBookmarksSize by intPreference( appContext.getPreferenceKey(R.string.pref_key_mobile_bookmarks_size), 0, @@ -1647,7 +1653,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { var showUnifiedSearchFeature by lazyFeatureFlagPreference( key = appContext.getPreferenceKey(R.string.pref_key_show_unified_search_2), default = { FxNimbus.features.unifiedSearch.value().enabled }, - featureFlag = FeatureFlags.unifiedSearchFeature, + featureFlag = true, ) /** @@ -1664,7 +1670,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { var notificationPrePermissionPromptEnabled by lazyFeatureFlagPreference( key = appContext.getPreferenceKey(R.string.pref_key_notification_pre_permission_prompt_enabled), default = { FxNimbus.features.prePermissionNotificationPrompt.value().enabled }, - featureFlag = FeatureFlags.notificationPrePermissionPromptEnabled, + featureFlag = true, ) /** diff --git a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt index efcbed05c..40684801a 100644 --- a/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt +++ b/app/src/main/java/org/mozilla/fenix/wallpapers/Wallpaper.kt @@ -60,11 +60,11 @@ data class Wallpaper( const val defaultName = "default" /* - Note: this collection could get out of sync with the version of it generated when fetching - remote metadata. It is included mostly for convenience, but use with utmost care until - we find a better way of handling the edge cases around this collection. It is generally - safer to do comparison directly with the collection name. - */ + * Note: this collection could get out of sync with the version of it generated when fetching + * remote metadata. It is included mostly for convenience, but use with utmost care until + * we find a better way of handling the edge cases around this collection. It is generally + * safer to do comparison directly with the collection name. + */ const val classicFirefoxCollectionName = "classic-firefox" val ClassicFirefoxCollection = Collection( name = classicFirefoxCollectionName, diff --git a/app/src/main/res/drawable-v23/splash_screen.xml b/app/src/main/res/drawable-v23/splash_screen.xml new file mode 100644 index 000000000..74d441577 --- /dev/null +++ b/app/src/main/res/drawable-v23/splash_screen.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/animated_splash_screen.xml b/app/src/main/res/drawable/animated_splash_screen.xml new file mode 100644 index 000000000..4e96325db --- /dev/null +++ b/app/src/main/res/drawable/animated_splash_screen.xml @@ -0,0 +1,1033 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_print.xml b/app/src/main/res/drawable/ic_print.xml new file mode 100644 index 000000000..0580d4ed4 --- /dev/null +++ b/app/src/main/res/drawable/ic_print.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_shopping_cart.xml b/app/src/main/res/drawable/ic_shopping_cart.xml new file mode 100644 index 000000000..ff274f022 --- /dev/null +++ b/app/src/main/res/drawable/ic_shopping_cart.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/mozac_ic_extensions_black.xml b/app/src/main/res/drawable/mozac_ic_extensions_black.xml deleted file mode 100644 index 9395d6040..000000000 --- a/app/src/main/res/drawable/mozac_ic_extensions_black.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/about_list_item.xml b/app/src/main/res/layout/about_list_item.xml index 184d7c686..c27d7d3bb 100644 --- a/app/src/main/res/layout/about_list_item.xml +++ b/app/src/main/res/layout/about_list_item.xml @@ -17,7 +17,6 @@ android:layout_width="0dp" android:layout_height="0dp" android:gravity="center" - android:textAlignment="center" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/component_collection_creation.xml b/app/src/main/res/layout/component_collection_creation.xml index e621d9000..185628e81 100644 --- a/app/src/main/res/layout/component_collection_creation.xml +++ b/app/src/main/res/layout/component_collection_creation.xml @@ -179,7 +179,6 @@ android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" android:text="@string/create_collection_save" - android:textAlignment="center" android:textColor="?textActionPrimary" android:textStyle="bold" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/component_collection_creation_name_collection.xml b/app/src/main/res/layout/component_collection_creation_name_collection.xml index edc793f12..2f9ef68a2 100644 --- a/app/src/main/res/layout/component_collection_creation_name_collection.xml +++ b/app/src/main/res/layout/component_collection_creation_name_collection.xml @@ -36,7 +36,6 @@ android:alpha="0" android:background="?android:attr/selectableItemBackgroundBorderless" android:text="@string/create_collection_select_all" - android:textAlignment="center" android:textAllCaps="false" android:textColor="@color/fx_mobile_text_color_oncolor_primary" android:textSize="16sp" @@ -154,7 +153,6 @@ android:layout_height="wrap_content" android:background="?android:attr/selectableItemBackground" android:text="@string/create_collection_save" - android:textAlignment="center" android:textAppearance="@style/TextAppearance.MaterialComponents.Button" android:textColor="?textActionPrimary" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/component_collection_creation_select_collection.xml b/app/src/main/res/layout/component_collection_creation_select_collection.xml index 75521a7a7..e8ecf455b 100644 --- a/app/src/main/res/layout/component_collection_creation_select_collection.xml +++ b/app/src/main/res/layout/component_collection_creation_select_collection.xml @@ -36,7 +36,6 @@ android:alpha="0" android:background="?android:attr/selectableItemBackgroundBorderless" android:text="@string/create_collection_select_all" - android:textAlignment="center" android:textAllCaps="false" android:textColor="@color/fx_mobile_text_color_oncolor_primary" android:textSize="16sp" @@ -159,7 +158,6 @@ android:alpha="0.0" android:background="?android:attr/selectableItemBackground" android:text="@string/create_collection_save" - android:textAlignment="center" android:textAppearance="@style/TextAppearance.MaterialComponents.Button" android:textColor="?textActionPrimary" android:visibility="gone" diff --git a/app/src/main/res/layout/component_downloads.xml b/app/src/main/res/layout/component_downloads.xml index c8a802b44..8a0e809d3 100644 --- a/app/src/main/res/layout/component_downloads.xml +++ b/app/src/main/res/layout/component_downloads.xml @@ -26,7 +26,6 @@ android:layout_height="0dp" android:gravity="center" android:text="@string/download_empty_message_1" - android:textAlignment="center" android:textColor="?attr/textSecondary" android:textSize="16sp" android:visibility="gone" diff --git a/app/src/main/res/layout/component_history.xml b/app/src/main/res/layout/component_history.xml index 26ec21397..30f8831b9 100644 --- a/app/src/main/res/layout/component_history.xml +++ b/app/src/main/res/layout/component_history.xml @@ -40,7 +40,6 @@ android:layout_height="0dp" android:gravity="center" android:text="@string/history_empty_message" - android:textAlignment="center" android:textColor="?attr/textSecondary" android:textSize="16sp" android:visibility="gone" diff --git a/app/src/main/res/layout/component_history_metadata_group.xml b/app/src/main/res/layout/component_history_metadata_group.xml index 50526f763..2bd63c648 100644 --- a/app/src/main/res/layout/component_history_metadata_group.xml +++ b/app/src/main/res/layout/component_history_metadata_group.xml @@ -14,7 +14,6 @@ android:layout_height="0dp" android:gravity="center" android:text="@string/history_empty_message" - android:textAlignment="center" android:textColor="?attr/textSecondary" android:textSize="16sp" android:visibility="gone" diff --git a/app/src/main/res/layout/fenix_snackbar.xml b/app/src/main/res/layout/fenix_snackbar.xml index 9364f7bdc..c4757c35b 100644 --- a/app/src/main/res/layout/fenix_snackbar.xml +++ b/app/src/main/res/layout/fenix_snackbar.xml @@ -51,7 +51,7 @@ android:minHeight="48dp" android:paddingTop="8dp" android:paddingBottom="8dp" - android:textAlignment="center" + android:textAlignment="textEnd" android:textAllCaps="true" android:textColor="@color/photonWhite" android:textSize="14sp" diff --git a/app/src/main/res/layout/fragment_add_ons_management.xml b/app/src/main/res/layout/fragment_add_ons_management.xml index bdd07cbaa..433d4f4d6 100644 --- a/app/src/main/res/layout/fragment_add_ons_management.xml +++ b/app/src/main/res/layout/fragment_add_ons_management.xml @@ -15,14 +15,6 @@ android:layout_marginTop="2dp" tools:context=".BrowserActivity" /> - - diff --git a/app/src/main/res/layout/fragment_address_editor.xml b/app/src/main/res/layout/fragment_address_editor.xml index 82d559b65..7c16e56e9 100644 --- a/app/src/main/res/layout/fragment_address_editor.xml +++ b/app/src/main/res/layout/fragment_address_editor.xml @@ -438,7 +438,6 @@ android:letterSpacing="0" android:padding="10dp" android:text="@string/addressess_delete_address_button" - android:textAlignment="center" android:textAllCaps="false" android:textColor="@color/fx_mobile_text_color_warning" android:visibility="gone" @@ -454,7 +453,6 @@ android:letterSpacing="0" android:padding="10dp" android:text="@string/addresses_cancel_button" - android:textAlignment="center" android:textAllCaps="false" android:textColor="?attr/textPrimary" android:textStyle="bold" diff --git a/app/src/main/res/layout/fragment_credit_card_editor.xml b/app/src/main/res/layout/fragment_credit_card_editor.xml index 76211b90b..c5b94bb7b 100644 --- a/app/src/main/res/layout/fragment_credit_card_editor.xml +++ b/app/src/main/res/layout/fragment_credit_card_editor.xml @@ -178,7 +178,6 @@ android:letterSpacing="0" android:padding="10dp" android:text="@string/credit_cards_delete_card_button" - android:textAlignment="center" android:textAllCaps="false" android:textColor="@color/fx_mobile_text_color_warning" android:visibility="gone" @@ -193,7 +192,6 @@ android:letterSpacing="0" android:padding="10dp" android:text="@string/credit_cards_cancel_button" - android:textAlignment="center" android:textAllCaps="false" android:textColor="?attr/textPrimary" android:textStyle="bold" diff --git a/app/src/main/res/layout/fragment_pwa_onboarding.xml b/app/src/main/res/layout/fragment_pwa_onboarding.xml index 2ee142cbb..585d1606a 100644 --- a/app/src/main/res/layout/fragment_pwa_onboarding.xml +++ b/app/src/main/res/layout/fragment_pwa_onboarding.xml @@ -84,7 +84,6 @@ android:layout_height="wrap_content" android:letterSpacing="0" android:text="@string/add_to_homescreen_continue" - android:textAlignment="center" android:textAllCaps="false" android:textColor="?attr/textPrimary" android:textSize="16sp" diff --git a/app/src/main/res/layout/fragment_share.xml b/app/src/main/res/layout/fragment_share.xml index f7a284739..510237a3e 100644 --- a/app/src/main/res/layout/fragment_share.xml +++ b/app/src/main/res/layout/fragment_share.xml @@ -56,7 +56,6 @@ + + diff --git a/app/src/main/res/layout/overlay_add_on_progress.xml b/app/src/main/res/layout/overlay_add_on_progress.xml deleted file mode 100644 index d12dc2027..000000000 --- a/app/src/main/res/layout/overlay_add_on_progress.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - -