diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 79e5c2939..58b4bf9b0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -23,3 +23,21 @@ /automation/ @mozilla-mobile/releng @mozilla-mobile/fenix /taskcluster/ /@mozilla-mobile/releng @mozilla-mobile/fenix /.github/ @mozilla-mobile/releng @mozilla-mobile/fenix + +# --- PERFORMANCE START --- # +# The performance team would like to monitor some files to understand +# when performance-impacting changes occur. Our intent is not to block +# these changes (for now) but to be aware of them. Please let us know +# if the CODEOWNERS system makes this impractical. We're available at +# #perf-android-frontend on Matrix. +/app/src/*/java/org/mozilla/fenix/perf/** @mozilla-mobile/Performance +*.pro @mozilla-mobile/Performance +*proguard* @mozilla-mobile/Performance + +# Possible startup regressions +*Application.kt @mozilla-mobile/Performance + +# We want to be aware of new features behind flags as well as features +# about to be enabled. +FeatureFlags.kt @mozilla-mobile/Performance +# --- PERFORMANCE END --- # diff --git a/.github/PULL_REQUEST_TEMPLATE b/.github/PULL_REQUEST_TEMPLATE index ef365cba2..35934de05 100644 --- a/.github/PULL_REQUEST_TEMPLATE +++ b/.github/PULL_REQUEST_TEMPLATE @@ -6,11 +6,8 @@ - [ ] **Screenshots**: This PR includes screenshots or GIFs of the changes made or an explanation of why it does not - [ ] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features. In addition, it includes a screenshot of a successful [accessibility scan](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en_US) to ensure no new defects are added to the product. -### After merge -- [ ] **Milestone**: Make sure issues finished by this pull request are added to the [milestone](https://github.com/mozilla-mobile/fenix/milestones) of the version currently in development. - ### To download an APK when reviewing a PR: 1. click on Show All Checks, 2. click Details next to "Taskcluster (pull_request)" after it appears and then finishes with a green checkmark, 3. click on the "Fenix - assemble" task, then click "Run Artifacts". -4. the APK links should be on the left side of the screen, named for each CPU architecture \ No newline at end of file +4. the APK links should be on the left side of the screen, named for each CPU architecture diff --git a/README.md b/README.md index b93808d9a..ef1d872d0 100644 --- a/README.md +++ b/README.md @@ -105,21 +105,23 @@ you want these variants to be: #### Performance Build Variants For accurate performance measurements, read this section! -If you want to analyze performance during **local development** (note: there is a non-trivial performance impact - see caveats): -- Recommendation: use a debuggable variant (see "local.properties helpers" below) with local Leanplum, Adjust, & Sentry API tokens: contact the front-end perf group for access to them -- Rationale: There are numerous performance-impacting differences between debug and release variants so we need a release variant. To profile, we also need debuggable, which is disabled by default for release variants. If API tokens are not provided, the SDKs may change their behavior in non-trivial ways. -- Caveats: - - debuggable has a non-trivial & variable impact on performance but is needed to take profiles. - - Random experiment opt-in & feature flags may impact performance (see [perf-frontend-issues#45](https://github.com/mozilla-mobile/perf-frontend-issues/issues/45) for mitigation). - - This is slower to build than debug builds because it does additional tasks (e.g. minification) similar to other release builds +To analyze performance during **local development** build a production variant locally (this could either be the Nightly, beta or release). Otherwise, you could also grab a pre-existing APK if you don't need to test some local changes. Then, use the Firefox profiler to profile what you need! -If you want to run **performance tests/benchmarks** in automation or locally: -- Recommendation: production builds. If debuggable is required, use recommendation above but note the caveat above. If your needs are not met, please contact the front-end perf group to identify a new solution. -- Rationale: like the rationale above, we need release variants so the choice is based on the debuggable flag. +For more information on how to use the profiler or how to use the build, refer to this [how to measure performance with the build](https://wiki.mozilla.org/Performance/How_to_get_started_on_Fenix) -For additional context on these recommendations, see [the perf build variant analysis](https://docs.google.com/document/d/1aW-m0HYncTDDiRz_2x6EjcYkjBpL9SHhhYix13Vil30/edit#). +If you want to run **performance tests/benchmarks** in automation or locally use a production build since it is much closer in behavior compared to what users see in the wild. -Before you can install any release variants, **you will need to sign them:** see [Automatically signing release builds](#automatically-sign-release-builds) for details. +Before you can install any release builds, **You will need to sign production build variants:** see [Automatically signing release builds](#automatically-sign-release-builds) for details. + +##### Known disabled-by-default features +Some features are disabled by default when Fenix is built locally. This can be problematic at times for checking performance since you might want to know how your code behaves with those features. +The known features that are disabled by default are: +- Sentry +- Leanplum +- Adjust +- Mozilla Location Services (also known as MLS) +- Firebase Push Services +- Telemetry (only disabled by default in debug builds) ## Pre-push hooks To reduce review turn-around time, we'd like all pushes to run tests locally. We'd diff --git a/app/build.gradle b/app/build.gradle index 44fa7a80b..2e22fbaca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -468,13 +468,10 @@ dependencies { implementation Deps.androidx_lifecycle_viewmodel implementation Deps.androidx_core implementation Deps.androidx_core_ktx - implementation Deps.androidx_dynamic_animation implementation Deps.androidx_transition implementation Deps.androidx_work_ktx implementation Deps.google_material - implementation Deps.google_flexbox - implementation Deps.lottie implementation Deps.adjust @@ -482,6 +479,9 @@ dependencies { implementation Deps.google_ads_id // Required for the Google Advertising ID + implementation Deps.google_play_store // Required for in-app reviews + implementation Deps.google_play_core_ktx // Required for in-app reviews + androidTestImplementation Deps.uiautomator // Removed pending AndroidX fixes androidTestImplementation "tools.fastlane:screengrab:2.0.0" diff --git a/app/lint.xml b/app/lint.xml index cf2226aea..aa99dd31f 100644 --- a/app/lint.xml +++ b/app/lint.xml @@ -7,6 +7,8 @@ + + diff --git a/app/metrics.yaml b/app/metrics.yaml index 230f67c14..202d3face 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -18,28 +18,50 @@ events: startups, not just cold startup. Note: There is a short gap between the time application goes into background and the time android reports the application going into the background. - Note: This metric does not cover the following cases: - Case # 1 -> a). open a link(for example, gmail) with in-app - Browser (metric report custom_tab startup) b). press home button - c). open gmail again (which brings us back to in app browser). - Step c will not report startup metric. Case # 2 -> a). open fenix - b). press home button c). launch fenix through app switcher/recent - apps. step c will not report startup type. + Note: This metric does not record souce when app opened from + task switcher: open application -> press home button -> open + recent tasks -> choose fenix. In this case will report + [source = unknown, type = hot, has_saved_instance_state = false]. extra_keys: + type: + description: | + the startup type for opening fenix. the application and HomeActivity + either needs to be created or started again. possible values are + `cold`, `warm`, `hot` or `error`. Error is for impossible cases. + Please file a bug if you see the error case. + app created AND HomeActivity created = cold + app started AND HomeActivity created = warm + app started AND HomeActivity started = hot + app created AND HomeActivity started = error source: description: | The method used to open Fenix. Possible values are `app_icon`, - `custom_tab`, `link` or `unknown` + `custom_tab`, `link` or `unknown`. unknown is for startup sources + where we can't pinpoint the cause. One UNKNOWN case is the app + switcher where we don't know what variables to check to ensure this + startup wasn't caused by something else. + has_saved_instance_state: + description: | + boolean value whether or not startup type has a savedInstance. + using savedInstance, HomeActivity's previous state can be restored. + This is an optional key since it is not applicable to all the cases. + for example, when we are doing a hot start up, we cant have a + savedInstanceState therefore we report only [APP_ICON, HOT] instead + of [APP_ICON, HOT, false]. bugs: - https://github.com/mozilla-mobile/fenix/issues/11830 + - https://github.com/mozilla-mobile/fenix/issues/12573 + - https://github.com/mozilla-mobile/fenix/pull/13494 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/12114#pullrequestreview-445245341 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 + - https://github.com/mozilla-mobile/fenix/pull/13494#pullrequestreview-474050499 data_sensitivity: - interaction notification_emails: - esmyth@mozilla.com - perf-android-fe@mozilla.com - expires: "2020-12-01" + expires: "2021-06-01" app_received_intent: type: event description: | @@ -62,10 +84,11 @@ events: - https://github.com/mozilla-mobile/fenix/issues/11830 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11940/ + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 notification_emails: - esmyth@mozilla.com - perf-android-fe@mozilla.com - expires: "2020-12-01" + expires: "2021-06-01" app_opened: type: event description: | @@ -80,12 +103,13 @@ events: - https://github.com/mozilla-mobile/fenix/issues/10616 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - telemetry-client-dev@mozilla.com - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" search_bar_tapped: type: event description: | @@ -99,11 +123,12 @@ events: - https://github.com/mozilla-mobile/fenix/issues/959 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" entered_url: type: event description: | @@ -117,11 +142,12 @@ events: - https://github.com/mozilla-mobile/fenix/issues/959 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" performed_search: type: event description: | @@ -141,11 +167,12 @@ events: data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1067#issuecomment-474598673 - https://github.com/mozilla-mobile/fenix/pull/1677 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" browser_menu_action: type: event description: | @@ -166,11 +193,12 @@ events: - https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708 - https://github.com/mozilla-mobile/fenix/pull/5098#issuecomment-529658996 - https://github.com/mozilla-mobile/fenix/pull/6310 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" total_uri_count: type: counter description: | @@ -191,7 +219,7 @@ events: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" preference_toggled: type: event description: | @@ -227,12 +255,13 @@ events: - https://github.com/mozilla-mobile/fenix/pull/6352 - https://github.com/mozilla-mobile/fenix/pull/6601 - https://github.com/mozilla-mobile/fenix/pull/6746 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-06-01" whats_new_tapped: type: event description: | @@ -241,11 +270,12 @@ events: - https://github.com/mozilla-mobile/fenix/issues/5021 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/5090 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" opened_link: type: event description: | @@ -253,16 +283,19 @@ events: extra_keys: mode: description: | - The mode the link was opened in. Either 'PRIVATE' or 'NORMAL'. + The mode the link was opened in. Either 'PRIVATE' or 'NORMAL'. N.B.: + this probe may be incorrectly implemented: see + https://github.com/mozilla-mobile/fenix/issues/14133 bugs: - https://github.com/mozilla-mobile/fenix/issues/5737 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/5975 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tab_counter_menu_action: type: event description: @@ -278,11 +311,12 @@ events: - https://github.com/mozilla-mobile/fenix/issues/11442 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11533 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" onboarding: fxa_auto_signin: @@ -298,7 +332,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" fxa_manual_signin: type: event description: @@ -312,7 +346,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" privacy_notice: type: event description: @@ -326,7 +360,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pref_toggled_private_browsing: type: event description: @@ -340,7 +374,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pref_toggled_toolbar_position: type: event description: @@ -359,7 +393,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pref_toggled_tracking_prot: type: event description: @@ -378,7 +412,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" whats_new: type: event description: @@ -392,7 +426,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pref_toggled_theme_picker: type: event description: @@ -411,7 +445,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" finish: type: event description: @@ -425,7 +459,7 @@ onboarding: notification_emails: - fenix-core@mozilla.com - erichards@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_shortcuts: selected: @@ -442,7 +476,7 @@ search_shortcuts: - https://github.com/mozilla-mobile/fenix/pull/1202#issuecomment-476870449 notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" toolbar_settings: changed_position: @@ -461,7 +495,7 @@ toolbar_settings: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" crash_reporter: opened: @@ -472,11 +506,12 @@ crash_reporter: - https://github.com/mozilla-mobile/fenix/issues/1040 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" closed: type: event description: | @@ -490,11 +525,12 @@ crash_reporter: - https://github.com/mozilla-mobile/fenix/issues/1040 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" context_menu: item_tapped: @@ -514,11 +550,12 @@ context_menu: - https://github.com/mozilla-mobile/fenix/issues/957 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1344#issuecomment-479285010 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" login_dialog: displayed: @@ -587,7 +624,7 @@ find_in_page: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -600,7 +637,7 @@ find_in_page: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" searched_page: type: event description: | @@ -613,7 +650,7 @@ find_in_page: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" metrics: default_browser: @@ -631,7 +668,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" top_sites_count: type: counter lifetime: application @@ -651,7 +688,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" has_top_sites: type: boolean lifetime: application @@ -667,7 +704,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" recently_used_pwa_count: type: counter lifetime: application @@ -736,7 +773,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" mozilla_products: type: string_list lifetime: application @@ -757,7 +794,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" default_moz_browser: type: string lifetime: application @@ -776,7 +813,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" adjust_campaign: type: string lifetime: application @@ -795,7 +832,7 @@ metrics: - technical notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" adjust_ad_group: type: string lifetime: application @@ -814,7 +851,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" adjust_creative: type: string lifetime: application @@ -833,7 +870,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" adjust_network: type: string lifetime: application @@ -852,7 +889,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" toolbar_position: type: string lifetime: application @@ -868,7 +905,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_widget_installed: type: boolean lifetime: application @@ -884,7 +921,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" tabs_open_count: type: counter lifetime: application @@ -904,7 +941,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" has_open_tabs: type: boolean lifetime: application @@ -920,7 +957,7 @@ metrics: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" preferences: show_search_suggestions: @@ -938,7 +975,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" remote_debugging: type: string_list description: > @@ -954,7 +991,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" telemetry: type: string_list description: > @@ -972,7 +1009,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" tracking_protection: type: string_list description: > @@ -989,7 +1026,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_bookmarks: type: string_list description: > @@ -1005,7 +1042,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_browsing_history: type: string_list description: > @@ -1021,7 +1058,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" show_clipboard_suggestions: type: string_list description: > @@ -1037,7 +1074,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" show_search_shortcuts: type: string_list description: > @@ -1053,7 +1090,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_links_in_a_private_tab: type: string_list description: > @@ -1069,7 +1106,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sync: type: string_list description: > @@ -1085,7 +1122,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sync_items: type: string_list description: > @@ -1103,7 +1140,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" show_voice_search: type: string_list description: > @@ -1119,7 +1156,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_suggestions_private: type: string_list description: > @@ -1136,7 +1173,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" toolbar_position: type: string_list description: > @@ -1152,7 +1189,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" accessibility_services: type: string_list description: > @@ -1169,7 +1206,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_links_in_app: type: string_list description: > @@ -1185,7 +1222,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" theme: type: string_list description: > @@ -1201,7 +1238,7 @@ preferences: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search.default_engine: code: @@ -1224,7 +1261,7 @@ search.default_engine: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" name: type: string lifetime: application @@ -1245,7 +1282,7 @@ search.default_engine: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" submission_url: type: string lifetime: application @@ -1267,7 +1304,7 @@ search.default_engine: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" bookmarks_management: open_in_new_tab: @@ -1278,11 +1315,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" open_in_new_tabs: type: event description: | @@ -1291,11 +1329,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" open_in_private_tab: type: event description: | @@ -1304,11 +1343,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" open_in_private_tabs: type: event description: | @@ -1317,11 +1357,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" edited: type: event description: | @@ -1330,11 +1371,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" moved: type: event description: | @@ -1343,11 +1385,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" removed: type: event description: | @@ -1356,11 +1399,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" multi_removed: type: event description: | @@ -1369,11 +1413,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" shared: type: event description: | @@ -1382,11 +1427,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" copied: type: event description: | @@ -1395,11 +1441,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" folder_add: type: event description: | @@ -1408,11 +1455,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/974 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1708 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" folder_remove: type: event description: | @@ -1421,11 +1469,12 @@ bookmarks_management: - https://github.com/mozilla-mobile/fenix/issues/3174 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3724 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" custom_tab: closed: @@ -1436,11 +1485,12 @@ custom_tab: - https://github.com/mozilla-mobile/fenix/issues/977 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1697 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" action_button: type: event description: | @@ -1449,11 +1499,12 @@ custom_tab: - https://github.com/mozilla-mobile/fenix/issues/977 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1697 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" menu: type: event description: | @@ -1462,11 +1513,12 @@ custom_tab: - https://github.com/mozilla-mobile/fenix/issues/977 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1697 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" activation: identifier: @@ -1482,11 +1534,12 @@ activation: - https://bugzilla.mozilla.org/1501822 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - highly_sensitive notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" activation_id: type: uuid lifetime: user @@ -1499,11 +1552,12 @@ activation: - https://bugzilla.mozilla.org/1538011 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - highly_sensitive notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" no_lint: - USER_LIFETIME_EXPIRATION @@ -1520,7 +1574,7 @@ qr_scanner: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" prompt_displayed: type: event description: | @@ -1534,7 +1588,7 @@ qr_scanner: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" navigation_allowed: type: event description: | @@ -1548,7 +1602,7 @@ qr_scanner: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" navigation_denied: type: event description: | @@ -1562,7 +1616,7 @@ qr_scanner: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" error_page: visited_error: @@ -1576,11 +1630,12 @@ error_page: - https://github.com/mozilla-mobile/fenix/issues/1242 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/2491#issuecomment-492414486 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" sync_auth: opened: @@ -1595,7 +1650,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -1608,7 +1663,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" use_email: type: event description: | @@ -1622,7 +1677,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" use_email_problem: type: event description: | @@ -1635,7 +1690,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sign_in: type: event description: | @@ -1649,7 +1704,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sign_out: type: event description: | @@ -1663,7 +1718,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sign_up: type: event description: | @@ -1677,7 +1732,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" paired: type: event description: | @@ -1692,7 +1747,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" auto_login: type: event description: | @@ -1707,7 +1762,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" recovered: type: event description: | @@ -1722,7 +1777,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" other_external: type: event description: | @@ -1737,7 +1792,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" scan_pairing: type: event description: | @@ -1750,7 +1805,7 @@ sync_auth: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sync_account: opened: @@ -1765,7 +1820,7 @@ sync_account: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -1778,7 +1833,7 @@ sync_account: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sync_now: type: event description: | @@ -1791,7 +1846,7 @@ sync_account: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" send_tab: type: event description: | @@ -1804,7 +1859,7 @@ sync_account: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" sign_in_to_send_tab: type: event description: | @@ -1817,7 +1872,7 @@ sync_account: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" history: opened: @@ -1832,7 +1887,7 @@ history: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" removed: type: event description: | @@ -1845,7 +1900,7 @@ history: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" removed_all: type: event description: | @@ -1858,7 +1913,7 @@ history: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" shared: type: event description: | @@ -1871,7 +1926,7 @@ history: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" opened_item: type: event description: | @@ -1884,7 +1939,7 @@ history: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" tip: displayed: @@ -1902,7 +1957,7 @@ tip: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pressed: type: event description: | @@ -1918,7 +1973,7 @@ tip: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -1934,7 +1989,7 @@ tip: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" reader_mode: available: @@ -1949,7 +2004,7 @@ reader_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" opened: type: event description: | @@ -1962,7 +2017,7 @@ reader_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -1975,7 +2030,7 @@ reader_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" appearance: type: event description: | @@ -1988,7 +2043,7 @@ reader_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" tabs_tray: opened: @@ -2003,7 +2058,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed: type: event description: | @@ -2016,7 +2071,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" opened_existing_tab: type: event description: | @@ -2029,7 +2084,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" closed_existing_tab: type: event description: | @@ -2042,7 +2097,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" private_mode_tapped: type: event description: | @@ -2055,7 +2110,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" normal_mode_tapped: type: event description: | @@ -2068,7 +2123,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" new_tab_tapped: type: event description: | @@ -2081,7 +2136,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" new_private_tab_tapped: type: event description: | @@ -2094,7 +2149,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" menu_opened: type: event description: | @@ -2107,7 +2162,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" save_to_collection: type: event description: | @@ -2121,7 +2176,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" share_all_tabs: type: event description: | @@ -2135,7 +2190,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" close_all_tabs: type: event description: | @@ -2149,7 +2204,7 @@ tabs_tray: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" collections: renamed: @@ -2160,12 +2215,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tab_restored: type: event description: | @@ -2174,12 +2230,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" all_tabs_restored: type: event description: | @@ -2188,12 +2245,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tab_removed: type: event description: | @@ -2202,12 +2260,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" shared: type: event description: | @@ -2216,12 +2275,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" removed: type: event description: | @@ -2230,12 +2290,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" saved: type: event description: | @@ -2249,12 +2310,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tabs_added: type: event description: | @@ -2268,12 +2330,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tab_select_opened: type: event description: | @@ -2283,12 +2346,13 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/3935 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - technical - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" add_tab_button: type: event description: | @@ -2297,11 +2361,12 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/4358 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" long_press: type: event description: | @@ -2310,11 +2375,12 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/4358 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" save_button: type: event description: | @@ -2325,11 +2391,12 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/4358 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" extra_keys: from_screen: description: | @@ -2343,11 +2410,12 @@ collections: - https://github.com/mozilla-mobile/fenix/issues/969 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/4539 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" search_widget: new_tab_button: @@ -2363,7 +2431,7 @@ search_widget: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" voice_button: type: event description: | @@ -2376,7 +2444,7 @@ search_widget: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_widget_cfr: displayed: @@ -2391,7 +2459,7 @@ search_widget_cfr: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" add_widget_pressed: type: event description: | @@ -2404,7 +2472,7 @@ search_widget_cfr: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" not_now_pressed: type: event description: | @@ -2417,7 +2485,7 @@ search_widget_cfr: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" canceled: type: event description: | @@ -2431,7 +2499,7 @@ search_widget_cfr: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" private_browsing_mode: garbage_icon: @@ -2447,7 +2515,7 @@ private_browsing_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" snackbar_undo: type: event description: | @@ -2461,7 +2529,7 @@ private_browsing_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" notification_tapped: type: event description: | @@ -2474,7 +2542,7 @@ private_browsing_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" notification_open: type: event description: | @@ -2487,7 +2555,7 @@ private_browsing_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" notification_delete: type: event description: | @@ -2501,7 +2569,7 @@ private_browsing_mode: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" contextual_hint.tracking_protection: display: @@ -2513,11 +2581,12 @@ contextual_hint.tracking_protection: - https://github.com/mozilla-mobile/fenix/issues/9625 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11923 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" dismiss: type: event description: | @@ -2528,11 +2597,12 @@ contextual_hint.tracking_protection: - https://github.com/mozilla-mobile/fenix/issues/9625 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11923 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" outside_tap: type: event description: | @@ -2542,11 +2612,12 @@ contextual_hint.tracking_protection: - https://github.com/mozilla-mobile/fenix/issues/9625 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11923 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" inside_tap: type: event description: | @@ -2556,11 +2627,12 @@ contextual_hint.tracking_protection: - https://github.com/mozilla-mobile/fenix/issues/9625 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11923 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" tracking_protection: exception_added: @@ -2576,7 +2648,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" panel_settings: type: event description: | @@ -2589,7 +2661,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" etp_shield: type: event description: | @@ -2602,7 +2674,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" etp_tracker_list: type: event description: | @@ -2616,7 +2688,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" etp_settings: type: event description: | @@ -2629,7 +2701,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" etp_setting_changed: type: event description: | @@ -2648,7 +2720,7 @@ tracking_protection: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" private_browsing_shortcut: create_shortcut: @@ -2663,7 +2735,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" cfr_add_shortcut: type: event description: | @@ -2677,7 +2749,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" cfr_cancel: type: event description: | @@ -2691,7 +2763,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pinned_shortcut_priv: type: event description: | @@ -2705,7 +2777,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" static_shortcut_tab: type: event description: | @@ -2719,7 +2791,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" static_shortcut_priv: type: event description: | @@ -2733,7 +2805,7 @@ private_browsing_shortcut: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" tab: media_play: @@ -2748,7 +2820,7 @@ tab: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" no_lint: - COMMON_PREFIX media_pause: @@ -2763,7 +2835,7 @@ tab: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" media_notification: play: @@ -2778,7 +2850,7 @@ media_notification: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pause: type: event description: | @@ -2791,7 +2863,7 @@ media_notification: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" media_state: play: @@ -2806,7 +2878,7 @@ media_state: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" pause: type: event description: | @@ -2819,7 +2891,7 @@ media_state: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" stop: type: event description: | @@ -2832,7 +2904,7 @@ media_state: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" logins: open_logins: @@ -2847,7 +2919,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_individual_login: type: event description: | @@ -2860,7 +2932,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" copy_login: type: event description: | @@ -2873,7 +2945,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" view_password_login: type: event description: | @@ -2886,7 +2958,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" save_logins_setting_changed: type: event description: | @@ -2904,7 +2976,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_login_editor: type: event description: | @@ -2917,7 +2989,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" delete_saved_login: type: event description: | @@ -2930,7 +3002,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" save_edited_login: type: event description: | @@ -2943,7 +3015,7 @@ logins: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" download_notification: resume: @@ -2954,11 +3026,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" pause: type: event description: | @@ -2967,11 +3040,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" cancel: type: event description: | @@ -2980,11 +3054,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" try_again: type: event description: | @@ -2994,11 +3069,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" open: type: event description: | @@ -3007,11 +3083,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" in_app_open: type: event description: | @@ -3020,11 +3097,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" in_app_try_again: type: event description: | @@ -3034,11 +3112,12 @@ download_notification: - https://github.com/mozilla-mobile/fenix/issues/5583 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/6554 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" user_specified_search_engines: custom_engine_added: @@ -3053,7 +3132,7 @@ user_specified_search_engines: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" no_lint: - COMMON_PREFIX custom_engine_deleted: @@ -3069,7 +3148,7 @@ user_specified_search_engines: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_suggestions: enable_in_private: @@ -3085,7 +3164,7 @@ search_suggestions: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" voice_search: tapped: @@ -3100,7 +3179,7 @@ voice_search: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" top_sites: open_default: @@ -3115,7 +3194,7 @@ top_sites: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_in_new_tab: type: event description: | @@ -3128,7 +3207,7 @@ top_sites: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" open_in_private_tab: type: event description: | @@ -3141,7 +3220,7 @@ top_sites: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" remove: type: event description: | @@ -3154,7 +3233,7 @@ top_sites: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" about_page: support_tapped: @@ -3165,11 +3244,12 @@ about_page: - https://github.com/mozilla-mobile/fenix/issues/6834 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8047 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" privacy_notice_tapped: type: event description: | @@ -3178,50 +3258,12 @@ about_page: - https://github.com/mozilla-mobile/fenix/issues/6834 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8047 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" - rights_tapped: - type: event - description: | - A user tapped on "Know your rights" item from About page - bugs: - - https://github.com/mozilla-mobile/fenix/issues/6834 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/8047 - data_sensitivity: - - interaction - notification_emails: - - fenix-core@mozilla.com - expires: "2020-10-01" - licensing_tapped: - type: event - description: | - A user tapped on "Licensing information" item from About page - bugs: - - https://github.com/mozilla-mobile/fenix/issues/6834 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/8047 - data_sensitivity: - - interaction - notification_emails: - - fenix-core@mozilla.com - expires: "2020-10-01" - libraries_tapped: - type: event - description: | - A user tapped on "Libraries that we use" item from About page - bugs: - - https://github.com/mozilla-mobile/fenix/issues/6834 - data_reviews: - - https://github.com/mozilla-mobile/fenix/pull/8047 - data_sensitivity: - - interaction - notification_emails: - - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" app_theme: dark_theme_selected: @@ -3237,11 +3279,12 @@ app_theme: - https://github.com/mozilla-mobile/fenix/issues/7289 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/7968 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-04-01" pocket: pocket_top_site_clicked: @@ -3256,7 +3299,7 @@ pocket: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" no_lint: - COMMON_PREFIX @@ -3272,7 +3315,7 @@ pocket: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" first_session: campaign: @@ -3290,7 +3333,7 @@ first_session: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" network: type: string send_in_pings: @@ -3306,7 +3349,7 @@ first_session: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" adgroup: type: string send_in_pings: @@ -3322,7 +3365,7 @@ first_session: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" creative: send_in_pings: - first-session @@ -3338,7 +3381,7 @@ first_session: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" timestamp: send_in_pings: - first-session @@ -3356,7 +3399,7 @@ first_session: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" browser.search: with_ads: @@ -3374,7 +3417,7 @@ browser.search: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" ad_clicks: type: labeled_counter description: | @@ -3390,7 +3433,7 @@ browser.search: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" in_content: type: labeled_counter description: | @@ -3405,7 +3448,7 @@ browser.search: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" addons: open_addons_in_settings: @@ -3416,11 +3459,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/6174 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8318 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" open_addon_in_toolbar_menu: type: event description: | @@ -3433,11 +3477,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/6174 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8318 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" has_installed_addons: type: boolean description: | @@ -3448,11 +3493,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/6174 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8318 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" has_enabled_addons: type: boolean description: | @@ -3463,11 +3509,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/6174 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/8318 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" installed_addons: type: string_list description: | @@ -3478,11 +3525,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/8920 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11080 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" enabled_addons: type: string_list description: | @@ -3493,11 +3541,12 @@ addons: - https://github.com/mozilla-mobile/fenix/issues/8920 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/11080 + - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 data_sensitivity: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2020-10-01" + expires: "2021-04-01" startup.timeline: framework_start: @@ -3524,7 +3573,7 @@ startup.timeline: notification_emails: - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" framework_start_error: send_in_pings: - startup-timeline @@ -3542,7 +3591,7 @@ startup.timeline: notification_emails: - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" framework_start_read_error: send_in_pings: - startup-timeline @@ -3560,7 +3609,7 @@ startup.timeline: notification_emails: - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" clock_ticks_per_second: send_in_pings: - startup-timeline @@ -3578,7 +3627,7 @@ startup.timeline: notification_emails: - perf-android-fe@mozilla.com - mcomella@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" perf.awesomebar: history_suggestions: @@ -3598,7 +3647,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" bookmark_suggestions: send_in_pings: - metrics @@ -3616,7 +3665,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" search_engine_suggestions: send_in_pings: - metrics @@ -3634,7 +3683,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" session_suggestions: send_in_pings: - metrics @@ -3652,7 +3701,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" synced_tabs_suggestions: send_in_pings: - metrics @@ -3670,7 +3719,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" clipboard_suggestions: send_in_pings: - metrics @@ -3688,7 +3737,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" shortcuts_suggestions: send_in_pings: - metrics @@ -3706,7 +3755,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-10-01" + expires: "2020-11-15" autoplay: visited_setting: 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 9beaf4524..2b47a32a0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt @@ -5,7 +5,9 @@ package org.mozilla.fenix.helpers import androidx.test.espresso.intent.rule.IntentsTestRule +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule +import androidx.test.uiautomator.UiDevice import org.mozilla.fenix.HomeActivity /** @@ -16,7 +18,12 @@ import org.mozilla.fenix.HomeActivity */ class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) : - ActivityTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) + ActivityTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) { + override fun beforeActivityLaunched() { + super.beforeActivityLaunched() + setLongTapTimeout() + } +} /** * A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity]. This adds @@ -26,5 +33,19 @@ class HomeActivityTestRule(initialTouchMode: Boolean = false, launchActivity: Bo * @param launchActivity See [IntentsTestRule] */ -class HomeActivityIntentTestRule(initialTouchMode: Boolean = false, launchActivity: Boolean = true) : - IntentsTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) +class HomeActivityIntentTestRule( + initialTouchMode: Boolean = false, + launchActivity: Boolean = true +) : + IntentsTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) { + override fun beforeActivityLaunched() { + super.beforeActivityLaunched() + setLongTapTimeout() + } +} + +// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click +fun setLongTapTimeout() { + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + mDevice.executeShellCommand("settings put secure long_press_timeout 3000") +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt index 1908835ab..61de022cf 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/RecyclerViewIdlingResource.kt @@ -3,13 +3,13 @@ package org.mozilla.fenix.helpers import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource.ResourceCallback -class RecyclerViewIdlingResource constructor(private val recycler: androidx.recyclerview.widget.RecyclerView) : +class RecyclerViewIdlingResource constructor(private val recycler: androidx.recyclerview.widget.RecyclerView, val minItemCount: Int = 0) : IdlingResource { private var callback: ResourceCallback? = null override fun isIdleNow(): Boolean { - if (recycler.adapter != null && recycler.adapter!!.itemCount > 0) { + if (recycler.adapter != null && recycler.adapter!!.itemCount > minItemCount) { if (callback != null) { callback!!.onTransitionToIdle() } 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 3bb2f5348..ddcdd7c84 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -113,7 +113,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) verifyBookmarkedURL(defaultWebPage.url.toString()) @@ -121,21 +121,19 @@ class BookmarksTest { } } - @Ignore("Intermittent failures: https://github.com/mozilla-mobile/fenix/issues/10911") @Test fun createBookmarkFolderTest() { homeScreen { }.openThreeDotMenu { }.openBookmarks { - clickAddFolderButton() - verifyKeyboardVisible() - addNewFolderName(bookmarksFolderName) - saveNewFolder() - bookmarksListIdlingResource = RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) + clickAddFolderButton() + verifyKeyboardVisible() + addNewFolderName(bookmarksFolderName) + saveNewFolder() verifyFolderTitle(bookmarksFolderName) verifyKeyboardHidden() } @@ -163,7 +161,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) @@ -193,7 +191,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickCopy { @@ -210,7 +208,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickShare { @@ -230,7 +228,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickOpenInNewTab { @@ -249,7 +247,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { }.clickOpenInPrivateTab { @@ -268,9 +266,10 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { + IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) }.clickDelete { verifyDeleteSnackBarText() verifyUndoDeleteSnackBarButton() @@ -306,7 +305,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) longTapSelectItem(defaultWebPage.url) @@ -336,7 +335,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) longTapSelectItem(defaultWebPage.url) @@ -359,7 +358,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) longTapSelectItem(defaultWebPage.url) @@ -384,11 +383,12 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) longTapSelectItem(firstWebPage.url) longTapSelectItem(secondWebPage.url) + IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) openActionBarOverflowOrOptionsMenu(activityTestRule.activity) } @@ -410,7 +410,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) longTapSelectItem(defaultWebPage.url) @@ -466,7 +466,7 @@ class BookmarksTest { }.openThreeDotMenu { }.openBookmarks { bookmarksListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list)) + RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) }.openThreeDotMenu(defaultWebPage.url) { IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) 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 45de4045a..cef6dbeb2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -21,6 +21,7 @@ import org.mozilla.fenix.helpers.TestAssetHelper 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 import java.io.File /** @@ -92,10 +93,7 @@ class DownloadTest { } @Test - @Ignore("Temp disable flakey test - see: https://github.com/mozilla-mobile/fenix/issues/5462") fun testDownloadNotification() { - homeScreen { }.dismissOnboarding() - val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer) navigationToolbar { @@ -108,7 +106,13 @@ class DownloadTest { verifyDownloadPrompt() }.clickDownload { verifyDownloadNotificationPopup() - verifyDownloadNotificationShade() } + + mDevice.openNotification() + notificationShade { + verifySystemNotificationExists("Download completed") + } + // close notification shade before the next test + mDevice.pressBack() } } 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 c1b65d5f2..4add929b9 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/HistoryTest.kt @@ -254,11 +254,9 @@ class HistoryTest { navigationToolbar { }.enterURLAndEnterToBrowser(firstWebPage.url) { - }.openTabDrawer { }.openHomeScreen { } - - navigationToolbar { - }.enterURLAndEnterToBrowser(secondWebPage.url) { - mDevice.waitForIdle() + }.openTabDrawer { + }.openNewTab { + }.submitQuery(secondWebPage.url.toString()) { }.openThreeDotMenu { }.openHistory { longTapSelectItem(firstWebPage.url) 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 a57ec57c7..11454a188 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SearchTest.kt @@ -57,7 +57,7 @@ class SearchTest { }.goBack { }.goBack { }.openSearch { - verifySearchWithText() +// verifySearchWithText() clickSearchEngineButton("DuckDuckGo") typeSearch("mozilla") verifySearchEngineResults("DuckDuckGo") 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 b9963cb98..26368bd0a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAboutTest.kt @@ -10,7 +10,6 @@ import okhttp3.mockwebserver.MockWebServer import org.junit.Rule import org.junit.Before import org.junit.After -import org.junit.Ignore import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityIntentTestRule @@ -69,7 +68,7 @@ class SettingsAboutTest { } } - @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13219") + @Test fun verifyAboutFirefoxPreview() { homeScreen { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt index ee20a2c55..4e1127875 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -191,7 +191,8 @@ class SettingsBasicsTest { }.enterURLAndEnterToBrowser(webpage) { checkTextSizeOnWebsite(textSizePercentage, fenixApp.components) }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openSettings { }.openAccessibilitySubMenu { 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 c146a5213..a963f5c51 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt @@ -194,7 +194,8 @@ class SettingsPrivacyTest { // Click save to save the login saveLoginFromPrompt("Save") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openSettings { TestHelper.scrollToElementByText("Logins and passwords") @@ -219,7 +220,8 @@ class SettingsPrivacyTest { // Don't save the login, add to exceptions saveLoginFromPrompt("Never save") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openSettings { }.openLoginsAndPasswordSubMenu { @@ -274,7 +276,7 @@ class SettingsPrivacyTest { browserScreen { }.openTabDrawer { verifyPrivateModeSelected() - }.openHomeScreen { } + }.openNewTab { }.dismiss { } setOpenLinksInPrivateOff() @@ -321,7 +323,7 @@ class SettingsPrivacyTest { clickAddAutomaticallyButton() }.openHomeScreenShortcut(pageShortcutName) { }.openTabDrawer { - }.openHomeScreen { } + }.openNewTab { }.dismiss { } setOpenLinksInPrivateOff() restartApp(activityTestRule) @@ -331,7 +333,8 @@ class SettingsPrivacyTest { }.searchAndOpenHomeScreenShortcut(pageShortcutName) { }.openTabDrawer { verifyNormalModeSelected() - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openSettings { }.openPrivateBrowsingSubMenu { 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 802a3a22b..086d08186 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -4,7 +4,6 @@ package org.mozilla.fenix.ui -import androidx.core.net.toUri import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import okhttp3.mockwebserver.MockWebServer @@ -16,6 +15,7 @@ import org.junit.Test import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.ui.robots.clickUrlbar import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -57,7 +57,8 @@ class SmokeTest { }.goBackToWebsite { }.openTabDrawer { verifyExistingTabList() - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyHomeScreen() } } @@ -105,7 +106,8 @@ class SmokeTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesTabs(defaultWebPage.title) }.openTabDrawer { }.openTab(defaultWebPage.title) { @@ -131,13 +133,11 @@ class SmokeTest { verifyUrl(defaultWebPage.url.toString()) }.openTabDrawer { closeTabViaXButton(defaultWebPage.title) - }.openHomeScreen { - navigationToolbar { - }.enterURLAndEnterToBrowser(youtubeUrl.toUri()) { - verifyBlueDot() - }.openThreeDotMenu { - verifyOpenInAppButton() - } + }.openNewTab { + }.submitQuery(youtubeUrl) { + verifyBlueDot() + }.openThreeDotMenu { + verifyOpenInAppButton() } } @@ -184,7 +184,8 @@ class SmokeTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { togglePrivateBrowsingModeOnOff() verifyExistingTopSitesTabs(defaultWebPage.title) togglePrivateBrowsingModeOnOff() @@ -208,13 +209,11 @@ class SmokeTest { verifyUrl(defaultWebPage.url.toString()) }.openTabDrawer { closeTabViaXButton(defaultWebPage.title) - }.openHomeScreen { - navigationToolbar { - }.enterURLAndEnterToBrowser(youtubeUrl.toUri()) { - verifyBlueDot() - }.openThreeDotMenu { - verifyOpenInAppButton() - } + }.openNewTab { + }.submitQuery(youtubeUrl) { + verifyBlueDot() + }.openThreeDotMenu { + verifyOpenInAppButton() } } } @@ -239,7 +238,8 @@ class SmokeTest { verifyUrl("webcompat.com/issues/new") verifyTabCounter("2") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openSettings { }.openEnhancedTrackingProtectionSubMenu { @@ -254,4 +254,60 @@ class SmokeTest { } } } + + @Test + fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() { + val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen { + }.openSearch { + verifyKeyboardVisibility() + clickSearchEngineButton() + verifySearchEngineList() + changeDefaultSearchEngine("Amazon.com") + verifySearchEngineIcon("Amazon.com") + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openTabDrawer { + }.openNewTab { + clickSearchEngineButton() + mDevice.waitForIdle() + changeDefaultSearchEngine("Bing") + verifySearchEngineIcon("Bing") + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openTabDrawer { + }.openNewTab { + clickSearchEngineButton() + mDevice.waitForIdle() + changeDefaultSearchEngine("DuckDuckGo") + verifySearchEngineIcon("DuckDuckGo") + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openTabDrawer { + }.openNewTab { + clickSearchEngineButton() + mDevice.waitForIdle() + changeDefaultSearchEngine("Twitter") + verifySearchEngineIcon("Twitter") + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openTabDrawer { + }.openNewTab { + clickSearchEngineButton() + changeDefaultSearchEngine("Wikipedia") + verifySearchEngineIcon("Wikipedia") + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openTabDrawer { + // Checking whether the next search will be with default or not + }.openNewTab { + }.goToSearchEngine { + }.enterURLAndEnterToBrowser(defaultWebPage.url) { + }.openNavigationToolbar { + clickUrlbar { + verifyDefaultSearchEngine("Google") + } + } + } } 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 059d47e4f..02a270524 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -149,92 +149,87 @@ class TabbedBrowsingTest { @Test fun closeTabTest() { - var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer) - - genericURLS.forEachIndexed { index, element -> - navigationToolbar { - }.openNewTabAndEnterToBrowser(element.url) { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - verifyCloseTabsButton("Test_Page_${index + 1}") - closeTabViaXButton("Test_Page_${index + 1}") - verifySnackBarText("Tab closed") - snackBarButtonClick("UNDO") - } + val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) - mDevice.waitForIdle() - - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - swipeTabRight("Test_Page_${index + 1}") - verifySnackBarText("Tab closed") - snackBarButtonClick("UNDO") - } + navigationToolbar { + }.openNewTabAndEnterToBrowser(genericURL.url) { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + verifyCloseTabsButton("Test_Page_1") + closeTabViaXButton("Test_Page_1") + verifySnackBarText("Tab closed") + snackBarButtonClick("UNDO") + } - mDevice.waitForIdle() + mDevice.waitForIdle() - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - swipeTabLeft("Test_Page_${index + 1}") - verifySnackBarText("Tab closed") - snackBarButtonClick("UNDO") - } + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + swipeTabRight("Test_Page_1") + verifySnackBarText("Tab closed") + snackBarButtonClick("UNDO") + } - mDevice.waitForIdle() + mDevice.waitForIdle() - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - }.openHomeScreen { - } + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + swipeTabLeft("Test_Page_1") + verifySnackBarText("Tab closed") + snackBarButtonClick("UNDO") } + + mDevice.waitForIdle() + + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + }.openNewTab { + }.dismiss { } } @Test fun closePrivateTabTest() { - var genericURLS = TestAssetHelper.getGenericAssets(mockWebServer) + val genericURL = TestAssetHelper.getGenericAsset(mockWebServer, 1) homeScreen { }.togglePrivateBrowsingMode() - genericURLS.forEachIndexed { index, element -> - navigationToolbar { - }.openNewTabAndEnterToBrowser(element.url) { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - verifyCloseTabsButton("Test_Page_${index + 1}") - closeTabViaXButton("Test_Page_${index + 1}") - verifySnackBarText("Private tab closed") - snackBarButtonClick("UNDO") - } + navigationToolbar { + }.openNewTabAndEnterToBrowser(genericURL.url) { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + verifyCloseTabsButton("Test_Page_1") + closeTabViaXButton("Test_Page_1") + verifySnackBarText("Private tab closed") + snackBarButtonClick("UNDO") + } - mDevice.waitForIdle() + mDevice.waitForIdle() - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - swipeTabRight("Test_Page_${index + 1}") - verifySnackBarText("Private tab closed") - snackBarButtonClick("UNDO") - } + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + swipeTabRight("Test_Page_1") + verifySnackBarText("Private tab closed") + snackBarButtonClick("UNDO") + } - mDevice.waitForIdle() + mDevice.waitForIdle() - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - swipeTabLeft("Test_Page_${index + 1}") - verifySnackBarText("Private tab closed") - snackBarButtonClick("UNDO") - } + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") + swipeTabLeft("Test_Page_1") + verifySnackBarText("Private tab closed") + snackBarButtonClick("UNDO") + } - mDevice.waitForIdle() + mDevice.waitForIdle() - browserScreen { - }.openTabDrawer { - verifyExistingOpenTabs("Test_Page_${index + 1}") - closeTabViaXButton("Test_Page_${index + 1}") - } + browserScreen { + }.openTabDrawer { + verifyExistingOpenTabs("Test_Page_1") } } @@ -290,8 +285,8 @@ class TabbedBrowsingTest { verifyTabTrayOverflowMenu(true) verifyExistingOpenTabs(defaultWebPage.title) verifyCloseTabsButton(defaultWebPage.title) - }.openHomeScreen { - } + }.openNewTab { + }.dismiss { } } @Test 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 67f3020bc..59e392a06 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt @@ -68,13 +68,14 @@ class ThreeDotMenuMainTest { }.openHelp { verifyHelpUrl() }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { }.openThreeDotMenu { }.openWhatsNew { verifyWhatsNewURL() }.openTabDrawer { - }.openHomeScreen { - } + }.openNewTab { + }.dismiss { } homeScreen { }.openThreeDotMenu { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt index 697e25ca6..674e767c2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt @@ -58,7 +58,8 @@ class TopSitesTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) } @@ -76,13 +77,15 @@ class TopSitesTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) }.openTopSiteTabWithTitle(title = defaultWebPageTitle) { verifyUrl(defaultWebPage.url.toString().replace("http://", "")) }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { @@ -105,7 +108,8 @@ class TopSitesTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { @@ -127,7 +131,8 @@ class TopSitesTest { }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { - }.openHomeScreen { + }.openNewTab { + }.dismiss { verifyExistingTopSitesList() verifyExistingTopSitesTabs(defaultWebPageTitle) }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { 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 3a37b6978..bd5a17f06 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 @@ -35,6 +35,7 @@ import org.hamcrest.Matchers.containsString import org.junit.Assert.assertEquals import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull @@ -52,10 +53,7 @@ class BookmarksRobot { fun verifyBookmarkedURL(url: String) = assertBookmarkURL(url) fun verifyFolderTitle(title: String) { - mDevice.waitNotNull( - Until.findObject(text(title)), - TestAssetHelper.waitingTime - ) + mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime) assertFolderTitle(title) } 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 5d6732cce..e07d7ba8c 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 @@ -12,12 +12,14 @@ import android.net.Uri import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.pressBack import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.matcher.BundleMatchers import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed 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 5f4377ff8..48ccc4c2e 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 @@ -37,8 +37,6 @@ class DownloadRobot { fun verifyDownloadNotificationPopup() = assertDownloadNotificationPopup() - fun verifyDownloadNotificationShade() = assertDownloadNotificationShade() - fun verifyPhotosAppOpens() = assertPhotosOpens() class Transition { @@ -98,17 +96,6 @@ private fun assertDownloadPrompt() { mDevice.waitNotNull(Until.findObjects(By.res("org.mozilla.fenix.debug:id/download_button"))) } -private fun assertDownloadNotificationShade() { - val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - mDevice.openNotification() - mDevice.waitNotNull( - Until.findObjects(By.text("Download completed")), TestAssetHelper.waitingTime - ) - - // Go home (no UIDevice closeNotification) to close notification shade - mDevice.pressHome() -} - private fun assertDownloadNotificationPopup() { val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) mDevice.waitNotNull(Until.findObjects(By.text("Open")), TestAssetHelper.waitingTime) 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 f1e24be71..8540e2a0a 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 @@ -553,11 +553,11 @@ private fun assertWelcomeHeader() = .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) private fun assertGetTheMostHeader() = - onView(allOf(withText("Get the most out of Firefox Preview."))) + onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account."))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) private fun assertAccountsSignInButton() = - onView(ViewMatchers.withResourceName("turn_on_sync_button")) + onView(ViewMatchers.withResourceName("fxa_sign_in_button")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) private fun assertGetToKnowHeader() = 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 556007c8f..184bf7eaf 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 @@ -26,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.Until +import kotlinx.android.synthetic.main.fragment_search_dialog.view.* import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.not @@ -239,12 +240,18 @@ fun navigationToolbar(interact: NavigationToolbarRobot.() -> Unit): NavigationTo return NavigationToolbarRobot.Transition() } +fun clickUrlbar(interact: SearchRobot.() -> Unit): SearchRobot.Transition { + urlBar().click() + SearchRobot().interact() + return SearchRobot.Transition() +} + private fun assertSuggestionsAreEqualTo(suggestionSize: Int, searchTerm: String) { mDevice.waitForIdle() awesomeBar().perform(typeText(searchTerm)) mDevice.waitForIdle() - onView(withId(R.id.awesomeBar)).check(suggestionsAreEqualTo(suggestionSize)) + onView(withId(R.id.awesome_bar)).check(suggestionsAreEqualTo(suggestionSize)) } private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String) { @@ -252,7 +259,7 @@ private fun assertSuggestionsAreMoreThan(suggestionSize: Int, searchTerm: String awesomeBar().perform(typeText(searchTerm)) mDevice.waitForIdle() - onView(withId(R.id.awesomeBar)).check(suggestionsAreGreaterThan(suggestionSize)) + onView(withId(R.id.awesome_bar)).check(suggestionsAreGreaterThan(suggestionSize)) } private fun assertNoHistoryBookmarks() { 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 df3e5c6fc..aea828072 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 @@ -1,11 +1,12 @@ package org.mozilla.fenix.ui.robots -import android.content.res.Resources import androidx.test.uiautomator.By.text +import androidx.test.uiautomator.UiObjectNotFoundException 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.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.ext.waitNotNull @@ -17,23 +18,21 @@ class NotificationRobot { UiSelector().resourceId("com.android.systemui:id/notification_stack_scroller") ) - mDevice.waitNotNull( - Until.hasObject(text(notificationMessage)), - waitingTime - ) + val notificationFound: Boolean + + notificationFound = try { + notificationTray().getChildByText( + UiSelector().text(notificationMessage), notificationMessage, true + ).exists() + } catch (e: UiObjectNotFoundException) { + false + } - var notificationFound = false - while (!notificationFound) { - try { - val notification = notificationTray().getChildByText( - UiSelector().text(notificationMessage), notificationMessage, - true - ) - notification.exists() - notificationFound = true - } catch (e: Resources.NotFoundException) { - e.printStackTrace() - } + if (!notificationFound) { + // swipe 2 times to expand the silent notifications on API 28 and higher, single-swipe doesn't do it + notificationTray().swipeUp(2) + val notification = mDevice.findObject(UiSelector().textContains(notificationMessage)) + assertTrue(notification.exists()) } } 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 8bfe0f56e..942848e76 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 @@ -6,6 +6,7 @@ package org.mozilla.fenix.ui.robots +import android.widget.ToggleButton import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView import androidx.test.espresso.ViewInteraction @@ -16,7 +17,9 @@ import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry @@ -28,8 +31,11 @@ import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.startsWith import org.hamcrest.Matchers +import org.junit.Assert.assertEquals import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull /** @@ -47,6 +53,24 @@ class SearchRobot { fun verifySearchSettings() = assertSearchSettings() fun verifySearchBarEmpty() = assertSearchBarEmpty() + fun verifyKeyboardVisibility() = assertKeyboardVisibility(isExpectedToBeVisible = true) + fun verifySearchEngineList() = assertSearchEngineList() + fun verifySearchEngineIcon(expectedText: String) { + onView(withContentDescription(expectedText)) + } + fun verifyDefaultSearchEngine(expectedText: String) = assertDefaultSearchEngine(expectedText) + + fun changeDefaultSearchEngine(searchEngineName: String) = + selectDefaultSearchEngine(searchEngineName) + + fun clickSearchEngineButton() { + val searchEngineButton = mDevice.findObject(UiSelector() + .instance(1) + .className(ToggleButton::class.java)) + searchEngineButton.waitForExists(waitingTime) + searchEngineButton.click() + } + fun clickScanButton() { scanButton().perform(click()) } @@ -82,10 +106,10 @@ class SearchRobot { fun scrollToSearchEngineSettings() { // Soft keyboard is visible on screen on view access; hide it - onView(allOf(withId(R.id.search_layout))).perform( + onView(allOf(withId(R.id.search_wrapper))).perform( closeSoftKeyboard() ) - onView(allOf(withId(R.id.awesomeBar))).perform(ViewActions.swipeUp()) + onView(allOf(withId(R.id.awesome_bar))).perform(ViewActions.swipeUp()) } fun clickSearchEngineSettings() { @@ -99,6 +123,13 @@ class SearchRobot { class Transition { val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + fun dismiss(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + mDevice.waitForIdle() + mDevice.pressBack() + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } + fun openBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { mDevice.waitForIdle() browserToolbarEditView().perform(typeText("mozilla\n")) @@ -106,10 +137,23 @@ class SearchRobot { BrowserRobot().interact() return BrowserRobot.Transition() } + + fun submitQuery(query: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + mDevice.waitForIdle() + browserToolbarEditView().perform(typeText(query + "\n")) + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + + fun goToSearchEngine(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition { + NavigationToolbarRobot().interact() + return NavigationToolbarRobot.Transition() + } } } -private fun awesomeBar() = onView(withId(R.id.awesomeBar)) +private fun awesomeBar() = onView(withId(R.id.awesome_bar)) private fun browserToolbarEditView() = onView(Matchers.allOf(withId(R.id.mozac_browser_toolbar_edit_url_view))) @@ -136,6 +180,8 @@ private fun scanButton(): ViewInteraction { private fun clearButton() = onView(withId(R.id.mozac_browser_toolbar_clear_view)) +private fun searchWrapper() = onView(withId(R.id.search_wrapper)) + private fun assertSearchEngineURL(searchEngineName: String) { mDevice.waitNotNull( Until.findObject(By.textContains("${searchEngineName.toLowerCase()}.com/?q=mozilla")), @@ -178,4 +224,46 @@ fun searchScreen(interact: SearchRobot.() -> Unit): SearchRobot.Transition { return SearchRobot.Transition() } +private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) = { + mDevice.waitNotNull( + Until.findObject( + By.text("Search Engine") + ), waitingTime + ) + assertEquals( + isExpectedToBeVisible, + UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + .executeShellCommand("dumpsys input_method | grep mInputShown") + .contains("mInputShown=true") + ) +} + +private fun assertSearchEngineList() { + onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click() + onView(withText("Google")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withText("Amazon.com")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withText("Bing")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withText("DuckDuckGo")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withText("Twitter")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + onView(withText("Wikipedia")) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} + +private fun selectDefaultSearchEngine(searchEngine: String) { + onView(withId(R.id.mozac_browser_toolbar_edit_icon)).click() + onView(withText(searchEngine)) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) + .perform(click()) +} + +private fun assertDefaultSearchEngine(expectedText: String) { + onView(allOf(withId(R.id.mozac_browser_toolbar_edit_icon), withContentDescription(expectedText))) + .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} + private fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt index 4e6a9e3ef..fe6986fe4 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuAboutRobot.kt @@ -222,7 +222,7 @@ private fun assertLibrariesUsed() { .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .perform(click()) - onView(withId(R.id.action_bar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries"))))) + onView(withId(R.id.navigationToolbar)).check(matches(hasDescendant(withText(containsString("Firefox Preview | OSS Libraries"))))) Espresso.pressBack() } 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 8acea8478..d13772c9c 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 @@ -135,12 +135,12 @@ class TabDrawerRobot { return BrowserRobot.Transition() } - fun openHomeScreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + fun openNewTab(interact: SearchRobot.() -> Unit): SearchRobot.Transition { mDevice.waitForIdle() newTabButton().perform(click()) - HomeScreenRobot().interact() - return HomeScreenRobot.Transition() + SearchRobot().interact() + return SearchRobot.Transition() } fun toggleToNormalTabs(interact: TabDrawerRobot.() -> Unit): Transition { 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 47ab12921..d77ab3513 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 @@ -164,7 +164,7 @@ class ThreeDotMenuMainRobot { fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition { onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) - mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) + mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime) bookmarksButton().click() BookmarksRobot().interact() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 554e11126..78a517e81 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -215,7 +215,8 @@ android:name=".crashes.CrashListActivity" android:exported="false" /> - + = Build.VERSION_CODES.O) { // required by StorageStatsMetrics. - taskQueue.runIfReadyOrQueue { + queue.runIfReadyOrQueue { // Because it may be slow to capture the storage stats, it might be preferred to // create a WorkManager task for this metric, however, I ran out of // implementation time and WorkManager is harder to test. @@ -217,6 +220,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } } + fun queueReviewPrompt() { + GlobalScope.launch(Dispatchers.IO) { + components.reviewPromptController.trackApplicationLaunch() + } + } + initQueue() // We init these items in the visual completeness queue to avoid them initing in the critical @@ -224,6 +233,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { queueInitExperiments() queueInitStorageAndServices() queueMetrics() + queueReviewPrompt() } private fun startMetricsIfEnabled() { @@ -257,14 +267,6 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // no-op, LeakCanary is disabled by default } - // This is for issue https://github.com/mozilla-mobile/fenix/issues/11660. We prefetch our info for startup - // so that we're sure that we have all the data available as our fragment is launched. - private fun prefetchForHomeFragment() { - StrictMode.allowThreadDiskReads().resetPoliciesAfter { - components.core.topSiteStorage.prefetch() - } - } - private fun setupPush() { // Sets the PushFeature as the singleton instance for push messages to go to. // We need the push feature setup here to deliver messages in the case where the service @@ -318,7 +320,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { runOnlyInMainProcess { components.core.icons.onTrimMemory(level) - components.core.sessionManager.onTrimMemory(level) + components.core.store.dispatch(SystemAction.LowMemoryAction(level)) } } diff --git a/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt b/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt index eb64e044b..0e31e5d93 100644 --- a/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt +++ b/app/src/main/java/org/mozilla/fenix/GlobalDirections.kt @@ -44,7 +44,7 @@ enum class GlobalDirections(val navDirections: NavDirections, val destinationId: R.id.deleteBrowsingDataFragment ), SettingsAddonManager( - NavGraphDirections.actionGlobalSettingsAddonsManagementFragment(), + NavGraphDirections.actionGlobalAddonsManagementFragment(), R.id.addonsManagementFragment ), SettingsLogins( diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 02496dca7..bc49ff745 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.os.StrictMode @@ -21,7 +22,6 @@ import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.PROTECTED import androidx.appcompat.app.ActionBar import androidx.appcompat.widget.Toolbar -import androidx.core.view.doOnPreDraw import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDestination import androidx.navigation.NavDirections @@ -40,13 +40,11 @@ import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.WebExtensionState -import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineView import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate import mozilla.components.feature.privatemode.notification.PrivateNotificationFeature import mozilla.components.feature.search.BrowserStoreSearchAdapter -import mozilla.components.feature.search.SearchAdapter import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.android.arch.lifecycle.addObservers @@ -56,7 +54,6 @@ import mozilla.components.support.ktx.android.content.share 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.RunWhenReadyQueue import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.webextensions.WebExtensionPopupFeature @@ -71,6 +68,7 @@ import org.mozilla.fenix.components.metrics.BreadcrumbsRecorder import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExceptionsFragmentDirections import org.mozilla.fenix.ext.alreadyOnDestination +import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav @@ -100,9 +98,11 @@ import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirection import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections import org.mozilla.fenix.tabtray.TabTrayDialogFragment +import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.BrowsersCache +import java.lang.ref.WeakReference /** * The main activity of the application. The application is primarily a single Activity (this one) @@ -121,7 +121,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private var isVisuallyComplete = false - private var visualCompletenessQueue: RunWhenReadyQueue? = null private var privateNotificationObserver: PrivateNotificationFeature? = null private var isToolbarInflated = false @@ -156,6 +155,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onCreate(savedInstanceState) } + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onCreate()", + data = mapOf( + "recreated" to (savedInstanceState != null).toString(), + "intent" to (intent?.action ?: "null") + ) + ) + components.publicSuffixList.prefetch() setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) @@ -163,13 +172,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // Must be after we set the content view if (isVisuallyComplete) { - rootContainer.doOnPreDraw { - // This delay is temporary. We are delaying 5 seconds until the performance - // team can locate the real point of visual completeness. - it.postDelayed({ - visualCompletenessQueue!!.ready() - }, delay) - } + components.performance.visualCompletenessQueue + .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer)) } sessionObserver = UriOpenedObserver(this) @@ -226,19 +230,33 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { captureSnapshotTelemetryMetrics() - setAppAllStartTelemetry(intent.toSafeIntent()) + startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null) StartupTimeline.onActivityCreateEndHome(this) // DO NOT MOVE ANYTHING BELOW HERE. } - protected open fun setAppAllStartTelemetry(safeIntent: SafeIntent) { - components.appAllSourceStartTelemetry.receivedIntentInHomeActivity(safeIntent) + protected open fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) { + components.appStartupTelemetry.onHomeActivityOnCreate(safeIntent, hasSavedInstanceState) + } + + override fun onRestart() { + super.onRestart() + + components.appStartupTelemetry.onHomeActivityOnRestart() } @CallSuper override fun onResume() { super.onResume() + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onResume()" + ) + + components.appStartupTelemetry.onHomeActivityOnResume() + components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue { lifecycleScope.launch { // Make sure accountManager is initialized. @@ -266,6 +284,29 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } + override fun onStart() { + super.onStart() + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onStart()" + ) + } + + override fun onStop() { + super.onStop() + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onStop()", + data = mapOf( + "finishing" to isFinishing.toString() + ) + ) + } + final override fun onPause() { if (settings().lastKnownMode.isPrivate) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) @@ -273,6 +314,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onPause() + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onPause()", + data = mapOf( + "finishing" to isFinishing.toString() + ) + ) + // Every time the application goes into the background, it is possible that the user // is about to change the browsers installed on their system. Therefore, we reset the cache of // all the installed browsers. @@ -283,9 +333,39 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { override fun onDestroy() { super.onDestroy() + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onDestroy()", + data = mapOf( + "finishing" to isFinishing.toString() + ) + ) + privateNotificationObserver?.stop() } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onConfigurationChanged()" + ) + } + + override fun recreate() { + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "recreate()" + ) + + super.recreate() + } + /** * Handles intents received when the activity is open. */ @@ -293,6 +373,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onNewIntent(intent) intent ?: return + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onNewIntent()", + data = mapOf( + "intent" to intent.action.toString() + ) + ) + val intentProcessors = listOf(CrashReporterIntentProcessor()) + externalSourceIntentProcessors val intentHandled = @@ -317,7 +406,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { .let(::getIntentAllSource) ?.also { components.analytics.metrics.track(Event.AppReceivedIntent(it)) } - setAppAllStartTelemetry(intent.toSafeIntent()) + components.appStartupTelemetry.onHomeActivityOnNewIntent(intent.toSafeIntent()) } /** @@ -331,7 +420,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { ): View? = when (name) { EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply { selectionActionDelegate = DefaultSelectionActionDelegate( - getSearchAdapter(components.core.store), + BrowserStoreSearchAdapter( + components.core.store, + tabId = getIntentSessionId(intent.toSafeIntent()) + ), resources = context.resources, shareTextClicked = { share(it) }, emailTextClicked = { email(it) }, @@ -424,9 +516,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onUserLeaveHint() } - protected open fun getSearchAdapter(store: BrowserStore): SearchAdapter = - BrowserStoreSearchAdapter(store) - protected open fun getBreadcrumbMessage(destination: NavDestination): String { val fragmentName = resources.getResourceEntryName(destination.id) return "Changing to fragment $fragmentName, isCustomTab: false" @@ -597,6 +686,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromLoginDetailFragment -> LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromTabTray -> + TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) } /** @@ -675,9 +766,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { * The root container is null at this point, so let the HomeActivity know that * we are visually complete. */ - fun postVisualCompletenessQueue(visualCompletenessQueue: RunWhenReadyQueue) { + fun setVisualCompletenessQueueReady() { isVisuallyComplete = true - this.visualCompletenessQueue = visualCompletenessQueue } private fun captureSnapshotTelemetryMetrics() = CoroutineScope(Dispatchers.IO).launch { @@ -714,7 +804,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { const val PRIVATE_BROWSING_MODE = "private_browsing_mode" const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open" const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open" - const val delay = 5000L const val START_IN_RECENTS_SCREEN = "start_in_recents_screen" // PWA must have been used within last 30 days to be considered "recently used" for the 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 e6bc5e57b..25fa9fc40 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -66,6 +66,12 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) } } + override fun onDestroyView() { + super.onDestroyView() + // letting go of the resources to avoid memory leak. + adapter = null + } + private fun bindRecyclerView(view: View) { val managementView = AddonsManagementView( navController = findNavController(), @@ -120,7 +126,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) addonNameTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context), addonSummaryTextColor = ThemeManager.resolveAttribute(R.attr.secondaryText, context), sectionsTypeFace = ResourcesCompat.getFont(context, R.font.metropolis_semibold), - addonBackgroundIconColor = ThemeManager.resolveAttribute(R.attr.inset, requireContext()), addonAllowPrivateBrowsingLabelDrawableRes = R.drawable.ic_add_on_private_browsing_label ) } 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 8a27c69ad..2cb6cc002 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -42,6 +42,7 @@ import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.state.SessionState 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.engine.prompt.ShareData import mozilla.components.feature.accounts.FxaCapability import mozilla.components.feature.accounts.FxaWebChannelFeature @@ -56,10 +57,10 @@ import mozilla.components.feature.privatemode.feature.SecureWindowFeature import mozilla.components.feature.prompts.PromptFeature import mozilla.components.feature.prompts.share.ShareDelegate import mozilla.components.feature.readerview.ReaderViewFeature +import mozilla.components.feature.search.SearchFeature import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.PictureInPictureFeature import mozilla.components.feature.session.SessionFeature -import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SwipeRefreshFeature import mozilla.components.feature.session.behavior.EngineViewBottomBehavior import mozilla.components.feature.sitepermissions.SitePermissions @@ -98,6 +99,7 @@ import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.accessibilityManager +import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.enterToImmersiveMode import org.mozilla.fenix.ext.getPreferenceKey @@ -133,6 +135,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session get() = _browserToolbarView!! protected val readerViewFeature = ViewBoundFeatureWrapper() + protected val thumbnailsFeature = ViewBoundFeatureWrapper() private val sessionFeature = ViewBoundFeatureWrapper() private val contextMenuFeature = ViewBoundFeatureWrapper() @@ -150,6 +153,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session private val secureWindowFeature = ViewBoundFeatureWrapper() private var fullScreenMediaFeature = ViewBoundFeatureWrapper() + private val searchFeature = ViewBoundFeatureWrapper() private var pipFeature: PictureInPictureFeature? = null var customTabSessionId: String? = null @@ -169,11 +173,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session require(arguments != null) customTabSessionId = arguments?.getString(EXTRA_SESSION_ID) - val view = if (FeatureFlags.browserChromeGestures) { - inflater.inflate(R.layout.browser_gesture_wrapper, container, false) - } else { - inflater.inflate(R.layout.fragment_browser, container, false) - } + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onCreateView()", + data = mapOf( + "customTabSessionId" to customTabSessionId.toString() + ) + ) + + val view = inflater.inflate(R.layout.fragment_browser, container, false) val activity = activity as HomeActivity activity.themeManager.applyStatusBarTheme(activity) @@ -212,6 +221,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session } return getSessionById()?.also { session -> + val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(HomeActivity.OPEN_TO_BROWSER, true) + } + val browserToolbarController = DefaultBrowserToolbarController( activity = requireActivity() as HomeActivity, navController = findNavController(), @@ -227,15 +241,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session swipeRefresh = swipeRefresh, browserAnimator = browserAnimator, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { - action = Intent.ACTION_VIEW - putExtra(HomeActivity.OPEN_TO_BROWSER, true) - }, + openInFenixIntent = openInFenixIntent, bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } }, scope = viewLifecycleOwner.lifecycleScope, tabCollectionStorage = requireComponents.core.tabCollectionStorage, - topSiteStorage = requireComponents.core.topSiteStorage, onTabCounterClicked = { + thumbnailsFeature.get()?.requestScreenshot() findNavController().nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() @@ -477,7 +488,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session }, onNeedToRequestPermissions = { permissions -> requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) - }), + }, + loginPickerView = if (FeatureFlags.loginSelect) loginSelectBar else null, + onManageLogins = { + browserAnimator.captureEngineViewAndDrawStatically { + val directions = + NavGraphDirections.actionGlobalSavedLoginsAuthFragment() + findNavController().navigate(directions) + } + } + ), owner = this, view = view ) @@ -486,7 +506,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session feature = SessionFeature( requireComponents.core.store, requireComponents.useCases.sessionUseCases.goBack, - requireComponents.useCases.engineSessionUseCases, view.engineView, customTabSessionId ), @@ -494,6 +513,26 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session view = view ) + searchFeature.set( + feature = SearchFeature(store, customTabSessionId) { request, tabId -> + val parentSession = sessionManager.findSessionById(tabId) + val useCase = if (request.isPrivate) { + requireComponents.useCases.searchUseCases.newPrivateTabSearch + } else { + requireComponents.useCases.searchUseCases.newTabSearch + } + + if (parentSession?.isCustomTabSession() == true) { + useCase.invoke(request.query) + requireActivity().startActivity(openInFenixIntent) + } else { + useCase.invoke(request.query, parentSession = parentSession) + } + }, + owner = this, + view = view + ) + val accentHighContrastColor = ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) @@ -541,7 +580,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session fullScreenFeature.set( feature = FullScreenFeature( requireComponents.core.store, - SessionUseCases(sessionManager), + requireComponents.useCases.sessionUseCases, customTabSessionId, ::viewportFitChange, ::fullScreenChanged @@ -593,7 +632,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session if (showEngineView) { engineView?.asView()?.isVisible = true - swipeRefresh.alpha = 1f + swipeRefresh?.alpha = 1f } else { engineView?.asView()?.isVisible = false } @@ -1063,11 +1102,38 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session */ override fun onDestroyView() { super.onDestroyView() + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onDestroyView()" + ) + requireContext().accessibilityManager.removeAccessibilityStateChangeListener(this) _browserToolbarView = null _browserInteractor = null } + override fun onAttach(context: Context) { + super.onAttach(context) + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onAttach()" + ) + } + + override fun onDetach() { + super.onDetach() + + // Diagnostic breadcrumb for "Display already aquired" crash: + // https://github.com/mozilla-mobile/android-components/issues/7960 + breadcrumb( + message = "onDetach()" + ) + } + companion object { private const val KEY_CUSTOM_TAB_SESSION_ID = "custom_tab_session_id" private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 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 9163d8ab5..913abac35 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -15,7 +15,6 @@ import androidx.core.content.ContextCompat import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.browser_gesture_wrapper.* import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -26,13 +25,11 @@ import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.readerview.ReaderViewFeature -import mozilla.components.feature.search.SearchFeature import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tabs.WindowFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.components.FenixSnackbar @@ -55,8 +52,6 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { private val windowFeature = ViewBoundFeatureWrapper() - private val searchFeature = ViewBoundFeatureWrapper() - private val thumbnailsFeature = ViewBoundFeatureWrapper() private var readerModeAvailable = false @@ -77,19 +72,15 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { val components = context.components return super.initializeUI(view)?.also { - // We need to wrap this whole thing in an if here because gestureLayout will not exist - // if the feature flag is off - if (FeatureFlags.browserChromeGestures) { - gestureLayout.addGestureListener( - ToolbarGestureHandler( - activity = requireActivity(), - contentLayout = browserLayout, - tabPreview = tabPreview, - toolbarLayout = browserToolbarView.view, - sessionManager = components.core.sessionManager - ) + gestureLayout.addGestureListener( + ToolbarGestureHandler( + activity = requireActivity(), + contentLayout = browserLayout, + tabPreview = tabPreview, + toolbarLayout = browserToolbarView.view, + sessionManager = components.core.sessionManager ) - } + ) val readerModeAction = BrowserToolbar.ToggleButton( @@ -148,23 +139,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { owner = this, view = view ) - searchFeature.set( - feature = SearchFeature(components.core.store) { - if (it.isPrivate) { - components.useCases.searchUseCases.newPrivateTabSearch.invoke( - it.query, - parentSession = getSessionById() - ) - } else { - components.useCases.searchUseCases.newTabSearch.invoke( - it.query, - parentSession = getSessionById() - ) - } - }, - owner = this, - view = view - ) } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt index cd507391b..a7bfe9d4d 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -6,19 +6,19 @@ package org.mozilla.fenix.browser import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator import android.app.Activity import android.graphics.PointF import android.graphics.Rect -import android.util.TypedValue import android.view.View import android.view.ViewConfiguration import androidx.annotation.Dimension import androidx.annotation.Dimension.DP +import androidx.core.animation.doOnEnd import androidx.core.graphics.contains import androidx.core.graphics.toPoint import androidx.core.view.isVisible -import androidx.dynamicanimation.animation.DynamicAnimation -import androidx.dynamicanimation.animation.FlingAnimation +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.support.ktx.android.util.dpToPx @@ -61,11 +61,6 @@ class ToolbarGestureHandler( private val touchSlop = ViewConfiguration.get(activity).scaledTouchSlop private val minimumFlingVelocity = ViewConfiguration.get(activity).scaledMinimumFlingVelocity - private val defaultVelocity = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - MINIMUM_ANIMATION_VELOCITY, - activity.resources.displayMetrics - ) private var gestureDirection = GestureDirection.LEFT_TO_RIGHT @@ -143,25 +138,12 @@ class ToolbarGestureHandler( ) { val destination = getDestination() if (destination is Destination.Tab && isGestureComplete(velocityX)) { - animateToNextTab(velocityX, destination.session) + animateToNextTab(destination.session) } else { animateCanceledGesture(velocityX) } } - private fun createFlingAnimation( - view: View, - minValue: Float, - maxValue: Float, - startVelocity: Float - ): FlingAnimation = - FlingAnimation(view, DynamicAnimation.TRANSLATION_X).apply { - setMinValue(minValue) - setMaxValue(maxValue) - setStartVelocity(startVelocity) - friction = ViewConfiguration.getScrollFriction() - } - private fun getDestination(): Destination { val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR val currentSession = sessionManager.selectedSession ?: return Destination.None @@ -234,73 +216,59 @@ class ToolbarGestureHandler( abs(velocityX) >= minimumFlingVelocity) } - private fun getVelocityFromFling(velocityX: Float): Float { - return max(abs(velocityX), defaultVelocity) + private fun getAnimator(finalContextX: Float, duration: Long): ValueAnimator { + return ValueAnimator.ofFloat(contentLayout.translationX, finalContextX).apply { + this.duration = duration + this.interpolator = LinearOutSlowInInterpolator() + addUpdateListener { animator -> + val value = animator.animatedValue as Float + contentLayout.translationX = value + tabPreview.translationX = when (gestureDirection) { + GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset + GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset + } + } + } } - private fun animateToNextTab(velocityX: Float, session: Session) { + private fun animateToNextTab(session: Session) { val browserFinalXCoordinate: Float = when (gestureDirection) { GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset } - val animationVelocity = when (gestureDirection) { - GestureDirection.RIGHT_TO_LEFT -> -getVelocityFromFling(velocityX) - GestureDirection.LEFT_TO_RIGHT -> getVelocityFromFling(velocityX) - } // Finish animating the contentLayout off screen and tabPreview on screen - createFlingAnimation( - view = contentLayout, - minValue = min(0f, browserFinalXCoordinate), - maxValue = max(0f, browserFinalXCoordinate), - startVelocity = animationVelocity - ).addUpdateListener { _, value, _ -> - tabPreview.translationX = when (gestureDirection) { - GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset - GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset + getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply { + doOnEnd { + contentLayout.translationX = 0f + sessionManager.select(session) + + // Fade out the tab preview to prevent flickering + val shortAnimationDuration = + activity.resources.getInteger(android.R.integer.config_shortAnimTime) + tabPreview.animate() + .alpha(0f) + .setDuration(shortAnimationDuration.toLong()) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator?) { + tabPreview.isVisible = false + } + }) } - }.addEndListener { _, _, _, _ -> - contentLayout.translationX = 0f - sessionManager.select(session) - - // Fade out the tab preview to prevent flickering - val shortAnimationDuration = - activity.resources.getInteger(android.R.integer.config_shortAnimTime) - tabPreview.animate() - .alpha(0f) - .setDuration(shortAnimationDuration.toLong()) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - tabPreview.isVisible = false - } - }) }.start() } - private fun animateCanceledGesture(gestureVelocity: Float) { - val velocity = if (getDestination() is Destination.None) { - defaultVelocity + private fun animateCanceledGesture(velocityX: Float) { + val duration = if (abs(velocityX) >= minimumFlingVelocity) { + CANCELED_FLING_ANIMATION_DURATION } else { - getVelocityFromFling(gestureVelocity) - }.let { v -> - when (gestureDirection) { - GestureDirection.RIGHT_TO_LEFT -> v - GestureDirection.LEFT_TO_RIGHT -> -v - } + CANCELED_GESTURE_ANIMATION_DURATION } - createFlingAnimation( - view = contentLayout, - minValue = min(0f, contentLayout.translationX), - maxValue = max(0f, contentLayout.translationX), - startVelocity = velocity - ).addUpdateListener { _, value, _ -> - tabPreview.translationX = when (gestureDirection) { - GestureDirection.RIGHT_TO_LEFT -> value + windowWidth + previewOffset - GestureDirection.LEFT_TO_RIGHT -> value - windowWidth - previewOffset + getAnimator(0f, duration).apply { + doOnEnd { + tabPreview.isVisible = false } - }.addEndListener { _, _, _, _ -> - tabPreview.isVisible = false }.start() } @@ -337,15 +305,24 @@ class ToolbarGestureHandler( private const val OVERSCROLL_HIDE_PERCENT = 0.20 /** - * The speed of the fling animation (in dp per second). + * The size of the gap between the tab preview and content layout. */ @Dimension(unit = DP) - private const val MINIMUM_ANIMATION_VELOCITY = 1500f + private const val PREVIEW_OFFSET = 48 /** - * The size of the gap between the tab preview and content layout. + * Animation duration when switching to another tab */ - @Dimension(unit = DP) - private const val PREVIEW_OFFSET = 48 + private const val FINISHED_GESTURE_ANIMATION_DURATION = 250L + + /** + * Animation duration gesture is canceled due to the swipe not being far enough + */ + private const val CANCELED_GESTURE_ANIMATION_DURATION = 200L + + /** + * Animation duration gesture is canceled due to a swipe in the opposite direction + */ + private const val CANCELED_FLING_ANIMATION_DURATION = 150L } } diff --git a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt index f4b5a7710..44f4131fe 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -32,7 +32,6 @@ import mozilla.components.service.fxa.sync.GlobalSyncableStoreProvider import mozilla.components.service.sync.logins.SyncableLoginsStorage import mozilla.components.support.utils.RunWhenReadyQueue import org.mozilla.fenix.Config -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController @@ -85,11 +84,8 @@ class BackgroundServices( ) @VisibleForTesting - val supportedEngines = if (FeatureFlags.syncedTabs) { + val supportedEngines = setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords, SyncEngine.Tabs) - } else { - setOf(SyncEngine.History, SyncEngine.Bookmarks, SyncEngine.Passwords) - } private val syncConfig = SyncConfig(supportedEngines, syncPeriodInMinutes = 240L) // four hours init { @@ -98,10 +94,7 @@ class BackgroundServices( GlobalSyncableStoreProvider.configureStore(SyncEngine.History to historyStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Bookmarks to bookmarkStorage) GlobalSyncableStoreProvider.configureStore(SyncEngine.Passwords to passwordsStorage) - - if (FeatureFlags.syncedTabs) { - GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage) - } + GlobalSyncableStoreProvider.configureStore(SyncEngine.Tabs to remoteTabsStorage) } private val telemetryAccountObserver = TelemetryAccountObserver( diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index 8528844b7..9e2b069f5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -18,7 +18,7 @@ import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.migration.state.MigrationStore import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.components.metrics.AppAllSourceStartTelemetry +import org.mozilla.fenix.components.metrics.AppStartupTelemetry import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Settings @@ -44,7 +44,7 @@ class Components(private val context: Context) { ) } val services by lazy { Services(context, backgroundServices.accountManager) } - val core by lazy { Core(context) } + val core by lazy { Core(context, analytics.crashReporter) } val search by lazy { Search(context) } val useCases by lazy { UseCases( @@ -53,7 +53,8 @@ class Components(private val context: Context) { core.sessionManager, core.store, search.searchEngineManager, - core.webAppShortcutManager + core.webAppShortcutManager, + core.topSiteStorage ) } val intentProcessors by lazy { @@ -83,7 +84,7 @@ class Components(private val context: Context) { } } - val appAllSourceStartTelemetry by lazy { AppAllSourceStartTelemetry(analytics.metrics) } + val appStartupTelemetry by lazy { AppStartupTelemetry(analytics.metrics) } @Suppress("MagicNumber") val addonUpdater by lazy { @@ -114,4 +115,11 @@ class Components(private val context: Context) { val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context.getSystemService()!!) } val settings by lazy { Settings(context) } + + val reviewPromptController by lazy { + ReviewPromptController( + context, + FenixReviewSettings(settings) + ) + } } 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 8df2e3f56..3b0a5bdb8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.components import GeckoProvider import android.content.Context import android.content.res.Configuration +import android.os.StrictMode import io.sentry.Sentry import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -15,7 +16,9 @@ import kotlinx.coroutines.withContext import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore @@ -37,6 +40,8 @@ import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.session.HistoryDelegate +import mozilla.components.feature.top.sites.DefaultTopSitesStorage +import mozilla.components.feature.top.sites.PinnedSiteStorage import mozilla.components.feature.webcompat.WebCompatFeature import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.feature.webnotifications.WebNotificationFeature @@ -46,16 +51,21 @@ import mozilla.components.service.digitalassetlinks.RelationChecker import mozilla.components.service.digitalassetlinks.local.StatementApi import mozilla.components.service.digitalassetlinks.local.StatementRelationChecker import mozilla.components.service.sync.logins.SyncableLoginsStorage +import mozilla.components.support.base.crash.CrashReporting +import mozilla.components.support.locale.LocaleManager import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings import org.mozilla.fenix.media.MediaService import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry +import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.settings.advanced.getSelectedLocale import org.mozilla.fenix.utils.Mockable import java.util.concurrent.TimeUnit @@ -63,7 +73,7 @@ import java.util.concurrent.TimeUnit * Component group for all core browser functionality. */ @Mockable -class Core(private val context: Context) { +class Core(private val context: Context, private val crashReporter: CrashReporting) { /** * The browser engine component initialized based on the build * configuration (see build variants). @@ -134,10 +144,14 @@ class Core(private val context: Context) { DownloadMiddleware(context, DownloadService::class.java), ReaderViewMiddleware(), ThumbnailsMiddleware(thumbnailStorage) - ) + ) + EngineMiddleware.create(engine, ::findSessionById) ) } + private fun findSessionById(tabId: String): Session? { + return sessionManager.findSessionById(tabId) + } + /** * The [CustomTabsServiceStore] holds global custom tabs related data. */ @@ -184,7 +198,7 @@ class Core(private val context: Context) { // Now that we have restored our previous state (if there's one) let's setup auto saving the state while // the app is used. - sessionStorage.autoSave(sessionManager) + sessionStorage.autoSave(store) .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) .whenGoingToBackground() .whenSessionsChange() @@ -228,7 +242,7 @@ class Core(private val context: Context) { // Use these for startup-path code, where we don't want to do any work that's not strictly necessary. // For example, this is how the GeckoEngine delegates (history, logins) are configured. // We can fully initialize GeckoEngine without initialized our storage. - val lazyHistoryStorage = lazy { PlacesHistoryStorage(context) } + val lazyHistoryStorage = lazy { PlacesHistoryStorage(context, crashReporter) } val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) } val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) } @@ -249,7 +263,46 @@ class Core(private val context: Context) { */ val thumbnailStorage by lazy { ThumbnailStorage(context) } - val topSiteStorage by lazy { TopSiteStorage(context) } + val pinnedSiteStorage by lazy { PinnedSiteStorage(context) } + + val topSiteStorage by lazy { + val defaultTopSites = mutableListOf>() + + StrictMode.allowThreadDiskReads().resetPoliciesAfter { + if (!context.settings().defaultTopSitesAdded) { + defaultTopSites.add( + Pair( + context.getString(R.string.default_top_site_google), + SupportUtils.GOOGLE_URL + ) + ) + + if (LocaleManager.getSelectedLocale(context).language == "en") { + defaultTopSites.add( + Pair( + context.getString(R.string.pocket_pinned_top_articles), + SupportUtils.POCKET_TRENDING_URL + ) + ) + } + + defaultTopSites.add( + Pair( + context.getString(R.string.default_top_site_wikipedia), + SupportUtils.WIKIPEDIA_URL + ) + ) + + context.settings().defaultTopSitesAdded = true + } + } + + DefaultTopSitesStorage( + pinnedSiteStorage, + historyStorage, + defaultTopSites + ) + } val permissionStorage by lazy { PermissionStorage(context) } @@ -273,7 +326,8 @@ class Core(private val context: Context) { getSecureAbove22Preferences().getString(PASSWORDS_KEY) ?: generateEncryptionKey(KEY_STRENGTH).also { if (context.settings().passwordsEncryptionKeyGenerated && - isSentryEnabled()) { + isSentryEnabled() + ) { // We already had previously generated an encryption key, but we have lost it Sentry.capture("Passwords encryption key for passwords storage was lost and we generated a new one") } @@ -290,7 +344,7 @@ class Core(private val context: Context) { fun getPreferredColorScheme(): PreferredColorScheme { val inDark = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == - Configuration.UI_MODE_NIGHT_YES + Configuration.UI_MODE_NIGHT_YES return when { context.settings().shouldUseDarkTheme -> PreferredColorScheme.Dark context.settings().shouldUseLightTheme -> PreferredColorScheme.Light 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 e8804e385..bd7a4c569 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FenixSnackbar.kt @@ -12,6 +12,7 @@ import android.widget.FrameLayout import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.ContentFrameLayout import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.updatePadding import androidx.core.widget.TextViewCompat import com.google.android.material.snackbar.BaseTransientBottomBar import com.google.android.material.snackbar.ContentViewCallback @@ -124,11 +125,8 @@ class FenixSnackbar private constructor( return FenixSnackbar(parent, content, callback, isError).also { it.duration = durationOrAccessibleDuration - it.view.setPadding( - 0, - 0, - 0, - if ( + it.view.updatePadding( + bottom = if ( isDisplayedWithBrowserToolbar && shouldUseBottomToolbar && // If the view passed in is a ContentFrameLayout, it does not matter diff --git a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt index 615d348a8..b692d216b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt @@ -5,10 +5,11 @@ package org.mozilla.fenix.components import mozilla.components.support.utils.RunWhenReadyQueue +import org.mozilla.fenix.perf.VisualCompletenessQueue /** * Component group for all functionality related to performance. */ class PerformanceComponent { - val visualCompletenessQueue by lazy { RunWhenReadyQueue() } + val visualCompletenessQueue by lazy { VisualCompletenessQueue(RunWhenReadyQueue()) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/ReviewPromptController.kt b/app/src/main/java/org/mozilla/fenix/components/ReviewPromptController.kt new file mode 100644 index 000000000..b7f81f361 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/ReviewPromptController.kt @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components + +import android.app.Activity +import android.content.Context +import androidx.annotation.VisibleForTesting +import com.google.android.play.core.ktx.launchReview +import com.google.android.play.core.ktx.requestReview +import com.google.android.play.core.review.ReviewManagerFactory +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.withContext +import org.mozilla.fenix.utils.Settings + +/** + * Interface that describes the settings needed to track the Review Prompt. + */ +interface ReviewSettings { + var numberOfAppLaunches: Int + val isDefaultBrowser: Boolean + var lastReviewPromptTimeInMillis: Long +} + +/** + * Wraps `Settings` to conform to `ReviewSettings`. + */ +class FenixReviewSettings( + val settings: Settings +) : ReviewSettings { + override var numberOfAppLaunches: Int + get() = settings.numberOfAppLaunches + set(value) { settings.numberOfAppLaunches = value } + override val isDefaultBrowser: Boolean + get() = settings.isDefaultBrowser() + override var lastReviewPromptTimeInMillis: Long + get() = settings.lastReviewPromptTimeInMillis + set(value) { settings.lastReviewPromptTimeInMillis = value } +} + +/** + * Controls the Review Prompt behavior. + */ +class ReviewPromptController( + private val context: Context, + private val reviewSettings: ReviewSettings, + private val timeNowInMillis: () -> Long = { System.currentTimeMillis() }, + private val tryPromptReview: suspend (Activity) -> Unit = { + val manager = ReviewManagerFactory.create(context) + val reviewInfo = manager.requestReview() + + withContext(Main) { + manager.launchReview(it, reviewInfo) + } + } +) { + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + @Volatile var reviewPromptIsReady = false + + suspend fun promptReview(activity: Activity) { + if (shouldShowPrompt()) { + tryPromptReview(activity) + reviewSettings.lastReviewPromptTimeInMillis = timeNowInMillis() + } + } + + fun trackApplicationLaunch() { + reviewSettings.numberOfAppLaunches = reviewSettings.numberOfAppLaunches + 1 + // We only want to show the the prompt after we've finished "launching" the application. + reviewPromptIsReady = true + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun shouldShowPrompt(): Boolean { + if (!reviewPromptIsReady) { + return false + } else { + // We only want to try to show it once to avoid unnecessary disk reads + reviewPromptIsReady = false + } + + if (!reviewSettings.isDefaultBrowser) { return false } + + val hasOpenedFiveTimes = reviewSettings.numberOfAppLaunches >= NUMBER_OF_LAUNCHES_REQUIRED + val now = timeNowInMillis() + val apprxFourMonthsAgo = now - (APPRX_MONTH_IN_MILLIS * NUMBER_OF_MONTHS_TO_PASS) + val lastPrompt = reviewSettings.lastReviewPromptTimeInMillis + val hasNotBeenPromptedLastFourMonths = lastPrompt == 0L || lastPrompt <= apprxFourMonthsAgo + + return hasOpenedFiveTimes && hasNotBeenPromptedLastFourMonths + } + + companion object { + private const val APPRX_MONTH_IN_MILLIS: Long = 1000L * 60L * 60L * 24L * 30L + private const val NUMBER_OF_LAUNCHES_REQUIRED = 5 + private const val NUMBER_OF_MONTHS_TO_PASS = 4 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/TopSiteStorage.kt b/app/src/main/java/org/mozilla/fenix/components/TopSiteStorage.kt deleted file mode 100644 index c7c0347bd..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/TopSiteStorage.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.components - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.asLiveData -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import mozilla.components.feature.top.sites.TopSite -import mozilla.components.feature.top.sites.TopSiteStorage -import mozilla.components.support.locale.LocaleManager -import org.mozilla.fenix.R -import org.mozilla.fenix.ext.observeOnce -import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.settings.SupportUtils -import org.mozilla.fenix.settings.advanced.getSelectedLocale -import org.mozilla.fenix.utils.Mockable - -@Mockable -class TopSiteStorage(private val context: Context) { - var cachedTopSites = listOf() - - val storage by lazy { - TopSiteStorage(context) - } - - init { - addDefaultTopSites() - } - - /** - * Adds a new [TopSite]. - */ - fun addTopSite(title: String, url: String, isDefault: Boolean = false) { - storage.addTopSite(title, url, isDefault) - } - - /** - * Returns a [LiveData] list of all the [TopSite] instances. - */ - fun getTopSites(): LiveData> { - return storage.getTopSites().asLiveData() - } - - /** - * Removes the given [TopSite]. - */ - fun removeTopSite(topSite: TopSite) { - storage.removeTopSite(topSite) - } - - private fun addDefaultTopSites() { - val topSiteCandidates = mutableListOf>() - if (!context.settings().defaultTopSitesAdded) { - topSiteCandidates.add( - Pair( - context.getString(R.string.default_top_site_google), - SupportUtils.GOOGLE_URL - ) - ) - - if (LocaleManager.getSelectedLocale(context).language == "en") { - topSiteCandidates.add( - Pair( - context.getString(R.string.pocket_pinned_top_articles), - SupportUtils.POCKET_TRENDING_URL - ) - ) - } - - topSiteCandidates.add( - Pair( - context.getString(R.string.default_top_site_wikipedia), - SupportUtils.WIKIPEDIA_URL - ) - ) - - GlobalScope.launch(Dispatchers.IO) { - topSiteCandidates.forEach { (title, url) -> - addTopSite(title, url, isDefault = true) - } - } - context.settings().defaultTopSitesAdded = true - } - } - - fun prefetch() { - getTopSites().observeOnce { - cachedTopSites = it - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt index 2cec0c741..9c05a060e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.components import android.content.Context import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.usecases.EngineSessionUseCases import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine import mozilla.components.feature.app.links.AppLinksUseCases @@ -20,6 +19,8 @@ import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.feature.top.sites.TopSitesStorage +import mozilla.components.feature.top.sites.TopSitesUseCases import org.mozilla.fenix.utils.Mockable /** @@ -34,12 +35,13 @@ class UseCases( private val sessionManager: SessionManager, private val store: BrowserStore, private val searchEngineManager: SearchEngineManager, - private val shortcutManager: WebAppShortcutManager + private val shortcutManager: WebAppShortcutManager, + private val topSitesStorage: TopSitesStorage ) { /** * Use cases that provide engine interactions for a given browser session. */ - val sessionUseCases by lazy { SessionUseCases(sessionManager) } + val sessionUseCases by lazy { SessionUseCases(store, sessionManager) } /** * Use cases that provide tab management. @@ -49,7 +51,7 @@ class UseCases( /** * Use cases that provide search engine integration. */ - val searchUseCases by lazy { SearchUseCases(context, searchEngineManager, sessionManager) } + val searchUseCases by lazy { SearchUseCases(context, store, searchEngineManager, sessionManager) } /** * Use cases that provide settings management. @@ -66,7 +68,10 @@ class UseCases( val contextMenuUseCases by lazy { ContextMenuUseCases(store) } - val engineSessionUseCases by lazy { EngineSessionUseCases(sessionManager) } - val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } + + /** + * Use cases that provide top sites management. + */ + val topSitesUseCase by lazy { TopSitesUseCases(topSitesStorage) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt deleted file mode 100644 index ce730d4f1..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/AppAllSourceStartTelemetry.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.components.metrics - -import android.content.Intent -import androidx.annotation.VisibleForTesting -import androidx.annotation.VisibleForTesting.PRIVATE -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleObserver -import androidx.lifecycle.OnLifecycleEvent -import androidx.lifecycle.ProcessLifecycleOwner -import mozilla.components.support.utils.SafeIntent - -/** - * Tracks how the application was opened through [Event.AppOpenedAllSourceStartup]. - * We only considered to be "opened" if it received an intent and the app was in the background. - */ -class AppAllSourceStartTelemetry(private val metrics: MetricController) : LifecycleObserver { - - // default value is true to capture the first launch of the application - private var wasApplicationInBackground = true - - init { - ProcessLifecycleOwner.get().lifecycle.addObserver(this) - } - - fun receivedIntentInExternalAppBrowserActivity(safeIntent: SafeIntent) { - setAppOpenedAllSourceFromIntent(safeIntent, true) - } - - fun receivedIntentInHomeActivity(safeIntent: SafeIntent) { - setAppOpenedAllSourceFromIntent(safeIntent, false) - } - - private fun setAppOpenedAllSourceFromIntent(intent: SafeIntent, isExternalAppBrowserActivity: Boolean) { - if (!wasApplicationInBackground) { - return - } - - val source = when { - isExternalAppBrowserActivity -> Event.AppOpenedAllSourceStartup.Source.CUSTOM_TAB - intent.isLauncherIntent -> Event.AppOpenedAllSourceStartup.Source.APP_ICON - intent.action == Intent.ACTION_VIEW -> Event.AppOpenedAllSourceStartup.Source.LINK - else -> Event.AppOpenedAllSourceStartup.Source.UNKNOWN - } - - metrics.track(Event.AppOpenedAllSourceStartup(source)) - - wasApplicationInBackground = false - } - - @OnLifecycleEvent(Lifecycle.Event.ON_STOP) - @VisibleForTesting(otherwise = PRIVATE) - fun onApplicationOnStop() { - wasApplicationInBackground = true - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/AppStartupTelemetry.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/AppStartupTelemetry.kt new file mode 100644 index 000000000..d5ab320cc --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AppStartupTelemetry.kt @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.components.metrics + +import android.content.Intent +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import androidx.lifecycle.ProcessLifecycleOwner +import mozilla.components.support.utils.SafeIntent +import org.mozilla.fenix.components.metrics.Event.AppAllStartup +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.APP_ICON +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.CUSTOM_TAB +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.LINK +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source.UNKNOWN +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD +import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM + +/** + * Tracks application startup source, type, and whether or not activity has savedInstance to restore + * the activity from. Sample metric = [source = COLD, type = APP_ICON, hasSavedInstance = false] + * The basic idea is to collect these metrics from different phases of startup through + * [AppAllStartup] and finally report them on Activity's onResume() function. + */ +@Suppress("TooManyFunctions") +class AppStartupTelemetry(private val metrics: MetricController) : LifecycleObserver { + + init { + ProcessLifecycleOwner.get().lifecycle.addObserver(this) + } + + private var isMetricRecordedSinceAppWasForegrounded = false + private var wasAppCreateCalledBeforeActivityCreate = false + + private var onCreateData: AppAllStartup? = null + private var onRestartData: Pair? = null + private var onNewIntentData: Source? = null + + fun onFenixApplicationOnCreate() { + wasAppCreateCalledBeforeActivityCreate = true + } + + fun onHomeActivityOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) { + setOnCreateData(safeIntent, hasSavedInstanceState, false) + } + + fun onExternalAppBrowserOnCreate(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) { + setOnCreateData(safeIntent, hasSavedInstanceState, true) + } + + fun onHomeActivityOnRestart() { + // we are not setting [Source] in this method since source is derived from an intent. + // therefore source gets set in onNewIntent(). + onRestartData = Pair(HOT, null) + } + + fun onHomeActivityOnNewIntent(safeIntent: SafeIntent) { + // we are only setting [Source] in this method since source is derived from an intent]. + // other metric fields are set in onRestart() + onNewIntentData = getStartupSourceFromIntent(safeIntent, false) + } + + private fun setOnCreateData( + safeIntent: SafeIntent, + hasSavedInstanceState: Boolean, + isExternalAppBrowserActivity: Boolean + ) { + onCreateData = AppAllStartup( + getStartupSourceFromIntent(safeIntent, isExternalAppBrowserActivity), + getAppStartupType(), + hasSavedInstanceState + ) + wasAppCreateCalledBeforeActivityCreate = false + } + + private fun getAppStartupType(): Type { + return if (wasAppCreateCalledBeforeActivityCreate) COLD else WARM + } + + private fun getStartupSourceFromIntent( + intent: SafeIntent, + isExternalAppBrowserActivity: Boolean + ): Source { + return when { + // since the intent action is same (ACTION_VIEW) for both CUSTOM_TAB and LINK. + // we have to make sure that we are checking for CUSTOM_TAB condition first as this + // check does not rely on intent action + isExternalAppBrowserActivity -> CUSTOM_TAB + intent.isLauncherIntent -> APP_ICON + intent.action == Intent.ACTION_VIEW -> LINK + // one of the unknown case is app switcher, where we go to the recent tasks to launch + // Fenix. + else -> UNKNOWN + } + } + + /** + * The reason we record metric on resume is because we need to wait for onNewIntent(), and + * we are not guaranteed that onNewIntent() will be called before or after onStart() / onRestart(). + * However we are guaranteed onResume() will be called after onNewIntent() and onStart(). Source: + * https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) + */ + fun onHomeActivityOnResume() { + recordMetric() + } + + private fun recordMetric() { + if (!isMetricRecordedSinceAppWasForegrounded) { + val appAllStartup: AppAllStartup = if (onCreateData != null) { + onCreateData!! + } else { + mergeOnRestartAndOnNewIntentIntoStartup() + } + metrics.track(appAllStartup) + isMetricRecordedSinceAppWasForegrounded = true + } + // we don't want any weird previous states to persist on our next metric record. + onCreateData = null + onNewIntentData = null + onRestartData = null + } + + private fun mergeOnRestartAndOnNewIntentIntoStartup(): AppAllStartup { + return AppAllStartup( + onNewIntentData ?: UNKNOWN, + onRestartData?.first ?: ERROR, + onRestartData?.second + ) + } + + @OnLifecycleEvent(Lifecycle.Event.ON_STOP) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + fun onApplicationOnStop() { + // application was backgrounded, we need to record the new metric type if + // application was to come to foreground again. + // Therefore we set the isMetricRecorded flag to false. + isMetricRecordedSinceAppWasForegrounded = false + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index 83d5ef93d..1ca03a57c 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -142,9 +142,6 @@ sealed class Event { object WhatsNewTapped : Event() object SupportTapped : Event() object PrivacyNoticeTapped : Event() - object RightsTapped : Event() - object LicensingTapped : Event() - object LibrariesThatWeUseTapped : Event() object PocketTopSiteClicked : Event() object PocketTopSiteRemoved : Event() object FennecToFenixMigrated : Event() @@ -319,11 +316,28 @@ sealed class Event { get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) } - data class AppOpenedAllSourceStartup(val source: Source) : Event() { + data class AppAllStartup( + val source: Source, + val type: Type, + val hasSavedInstanceState: Boolean? = null + ) : Event() { enum class Source { APP_ICON, LINK, CUSTOM_TAB, UNKNOWN } + enum class Type { COLD, WARM, HOT, ERROR } override val extras: Map? - get() = hashMapOf(Events.appOpenedAllStartupKeys.source to source.name) + get() { + val extrasMap = hashMapOf( + Events.appOpenedAllStartupKeys.source to source.toString(), + Events.appOpenedAllStartupKeys.type to type.toString() + ) + // we are only sending hasSavedInstanceState whenever we get data from + // activity's oncreate() method. + if (hasSavedInstanceState != null) { + extrasMap[Events.appOpenedAllStartupKeys.hasSavedInstanceState] = + hasSavedInstanceState.toString() + } + return extrasMap + } } data class CollectionSaveButtonPressed(val fromScreen: String) : Event() { @@ -488,7 +502,7 @@ sealed class Event { NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER, - BOOKMARKS, HISTORY, SYNC_TABS + BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS } override val extras: Map? diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index db2e02180..a89499fce 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -106,7 +106,7 @@ private val Event.wrapper: EventWrapper<*>? { Events.appReceivedIntent.record(it) }, { Events.appReceivedIntentKeys.valueOf(it) } ) - is Event.AppOpenedAllSourceStartup -> EventWrapper( + is Event.AppAllStartup -> EventWrapper( { Events.appOpenedAllStartup.record(it) }, { Events.appOpenedAllStartupKeys.valueOf(it) } ) @@ -531,15 +531,6 @@ private val Event.wrapper: EventWrapper<*>? is Event.PrivacyNoticeTapped -> EventWrapper( { AboutPage.privacyNoticeTapped.record(it) } ) - is Event.RightsTapped -> EventWrapper( - { AboutPage.rightsTapped.record(it) } - ) - is Event.LicensingTapped -> EventWrapper( - { AboutPage.licensingTapped.record(it) } - ) - is Event.LibrariesThatWeUseTapped -> EventWrapper( - { AboutPage.librariesTapped.record(it) } - ) is Event.PocketTopSiteClicked -> EventWrapper( { Pocket.pocketTopSiteClicked.record(it) } ) @@ -725,7 +716,7 @@ class GleanMetricsService(private val context: Context) : MetricsService { // The code below doesn't need to execute immediately, so we'll add them to the visual // completeness task queue to be run later. - context.components.performance.visualCompletenessQueue.runIfReadyOrQueue { + context.components.performance.visualCompletenessQueue.queue.runIfReadyOrQueue { // We have to initialize Glean *on* the main thread, because it registers lifecycle // observers. However, the activation ping must be sent *off* of the main thread, // because it calls Google ad APIs that must be called *off* of the main thread. diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt index d6b4737f6..acd4dae51 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.components.metrics import android.app.Application import android.content.Context.MODE_PRIVATE import android.net.Uri +import android.os.StrictMode import android.util.Log import androidx.annotation.VisibleForTesting import com.leanplum.Leanplum @@ -22,6 +23,7 @@ import kotlinx.coroutines.withContext import mozilla.components.support.locale.LocaleManager import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts +import org.mozilla.fenix.ext.resetPoliciesAfter import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor import java.util.Locale @@ -81,7 +83,9 @@ class LeanplumMetricsService( override val type = MetricServiceType.Marketing private val token = Token(LeanplumId, LeanplumToken) - private val preferences = application.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE) + private val preferences = StrictMode.allowThreadDiskReads().resetPoliciesAfter { + application.getSharedPreferences(PREFERENCE_NAME, MODE_PRIVATE) + } @VisibleForTesting internal val deviceId by lazy { diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt index 1694d89f0..6f2234948 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.components.metrics import androidx.annotation.VisibleForTesting +import com.leanplum.Leanplum import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.menu.facts.BrowserMenuFacts import mozilla.components.browser.toolbar.facts.ToolbarFacts @@ -193,6 +194,7 @@ internal class ReleaseMetricController( if (installedAddons is List<*>) { Addons.installedAddons.set(installedAddons.map { it.toString() }) Addons.hasInstalledAddons.set(installedAddons.size > 0) + Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size)) } } @@ -200,6 +202,7 @@ internal class ReleaseMetricController( if (enabledAddons is List<*>) { Addons.enabledAddons.set(enabledAddons.map { it.toString() }) Addons.hasEnabledAddons.set(enabledAddons.size > 0) + Leanplum.setUserAttributes(mapOf("enabled_addons" to enabledAddons.size)) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 5fd5b8ec6..9e0896ec6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -23,6 +23,7 @@ import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.feature.session.SessionFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.kotlin.isUrl +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R @@ -34,7 +35,6 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.components.TopSiteStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView @@ -75,13 +75,12 @@ class DefaultBrowserToolbarController( private val bookmarkTapped: (Session) -> Unit, private val scope: CoroutineScope, private val tabCollectionStorage: TabCollectionStorage, - private val topSiteStorage: TopSiteStorage, private val onTabCounterClicked: () -> Unit, private val onCloseTab: (Session) -> Unit ) : BrowserToolbarController { private val useNewSearchExperience - get() = activity.settings().useNewSearchExperience + get() = FeatureFlags.newSearchExperience private val currentSession get() = customTabSession ?: activity.components.core.sessionManager.selectedSession @@ -245,7 +244,9 @@ class DefaultBrowserToolbarController( scope.launch { ioScope.launch { currentSession?.let { - topSiteStorage.addTopSite(it.title, it.url) + with(activity.components.useCases.topSitesUseCase) { + addPinnedSites(it.title, it.url) + } } }.join() @@ -380,6 +381,13 @@ class DefaultBrowserToolbarController( BrowserFragmentDirections.actionGlobalHistoryFragment() ) } + + ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { + navController.nav( + R.id.browserFragment, + BrowserFragmentDirections.actionGlobalDownloadsFragment() + ) + } } } @@ -414,6 +422,7 @@ class DefaultBrowserToolbarController( ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY + ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS } activity.components.analytics.metrics.track(Event.BrowserMenuItemTapped(eventItem)) 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 dc98afa23..ff549ffa2 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 @@ -177,11 +177,14 @@ class DefaultToolbarMenu( ?.browsingModeManager?.mode == BrowsingMode.Normal val shouldDeleteDataOnQuit = context.components.settings .shouldDeleteBrowsingDataOnQuit + val syncedTabsInTabsTray = context.components.settings + .syncedTabsInTabsTray val menuItems = listOfNotNull( + if (FeatureFlags.viewDownloads) downloadsItem else null, historyItem, bookmarksItem, - if (FeatureFlags.syncedTabs) syncedTabs else null, + if (syncedTabsInTabsTray) null else syncedTabs, settings, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, BrowserMenuDivider(), @@ -333,6 +336,14 @@ class DefaultToolbarMenu( onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) } + val downloadsItem = BrowserMenuImageText( + "Downloads", + R.drawable.ic_download, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Downloads) + } + @ColorRes private fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt index 3677d6cde..fe015efa0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/TabCounter.kt @@ -12,6 +12,7 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.LayoutInflater import android.widget.RelativeLayout +import androidx.core.view.updatePadding import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.* import org.mozilla.fenix.R import java.text.NumberFormat @@ -178,7 +179,7 @@ class TabCounter @JvmOverloads constructor( private fun formatForDisplay(count: Int): String { return if (count > MAX_VISIBLE_TABS) { - counter_text.setPadding(0, 0, 0, INFINITE_CHAR_PADDING_BOTTOM) + counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM) SO_MANY_TABS_OPEN } else NumberFormat.getInstance().format(count.toLong()) } 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 5a8d88b13..27b47c309 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 @@ -30,6 +30,7 @@ interface ToolbarMenu { object ReaderModeAppearance : Item() object Bookmarks : Item() object History : Item() + object Downloads : Item() } val menuBuilder: BrowserMenuBuilder diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt index 0e079b512..40c64d5bd 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -4,19 +4,15 @@ package org.mozilla.fenix.customtabs -import android.content.Intent import androidx.navigation.NavDestination import androidx.navigation.NavDirections import mozilla.components.browser.session.runWithSession -import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.pwa.ext.getWebAppManifest -import mozilla.components.feature.search.SearchAdapter import mozilla.components.support.utils.SafeIntent import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components @@ -28,12 +24,6 @@ import java.security.InvalidParameterException */ open class ExternalAppBrowserActivity : HomeActivity() { - private val openInFenixIntent by lazy { - Intent(this, IntentReceiverActivity::class.java).apply { - action = Intent.ACTION_VIEW - } - } - final override fun getBreadcrumbMessage(destination: NavDestination): String { val fragmentName = resources.getResourceEntryName(destination.id) return "Changing to fragment $fragmentName, isCustomTab: true" @@ -45,8 +35,8 @@ open class ExternalAppBrowserActivity : HomeActivity() { final override fun getIntentSessionId(intent: SafeIntent) = intent.getSessionId() - override fun setAppAllStartTelemetry(safeIntent: SafeIntent) { - components.appAllSourceStartTelemetry.receivedIntentInExternalAppBrowserActivity(safeIntent) + override fun startupTelemetryOnCreateCalled(safeIntent: SafeIntent, hasSavedInstanceState: Boolean) { + components.appStartupTelemetry.onExternalAppBrowserOnCreate(safeIntent, hasSavedInstanceState) } override fun getNavDirections( @@ -73,19 +63,6 @@ open class ExternalAppBrowserActivity : HomeActivity() { } } - override fun getSearchAdapter(store: BrowserStore): SearchAdapter { - val baseAdapter = super.getSearchAdapter(store) - return object : SearchAdapter { - - override fun sendSearch(isPrivate: Boolean, text: String) { - baseAdapter.sendSearch(isPrivate, text) - startActivity(openInFenixIntent) - } - - override fun isPrivateSession() = baseAdapter.isPrivateSession() - } - } - override fun onDestroy() { super.onDestroy() diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt b/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt index 78d7955e6..f6e6facf2 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt @@ -4,9 +4,9 @@ package org.mozilla.fenix.customtabs +import android.app.Notification import android.content.Context import android.content.Intent -import androidx.core.app.NotificationCompat import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.manifest.WebAppManifest @@ -23,12 +23,8 @@ class WebAppSiteControlsBuilder( private val inner = SiteControlsBuilder.CopyAndRefresh(reloadUrlUseCase) - override fun buildNotification( - context: Context, - builder: NotificationCompat.Builder, - channelId: String - ) { - inner.buildNotification(context, builder, channelId) + override fun buildNotification(context: Context, builder: Notification.Builder) { + inner.buildNotification(context, builder) val isPrivateSession = sessionManager.findSessionById(sessionId)?.private ?: false diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt index b0050ecf8..3581ea988 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/ExceptionsAdapter.kt @@ -11,6 +11,7 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import mozilla.components.ui.widgets.WidgetSiteItemView import org.mozilla.fenix.exceptions.viewholders.ExceptionsDeleteButtonViewHolder import org.mozilla.fenix.exceptions.viewholders.ExceptionsHeaderViewHolder import org.mozilla.fenix.exceptions.viewholders.ExceptionsListItemViewHolder @@ -67,7 +68,7 @@ abstract class ExceptionsAdapter( ExceptionsHeaderViewHolder.LAYOUT_ID -> ExceptionsHeaderViewHolder(view, headerDescriptionResource) ExceptionsListItemViewHolder.LAYOUT_ID -> - ExceptionsListItemViewHolder(view, interactor) + ExceptionsListItemViewHolder(view as WidgetSiteItemView, interactor) else -> throw IllegalStateException() } } diff --git a/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt index d3225d34f..d403703e1 100644 --- a/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/exceptions/viewholders/ExceptionsListItemViewHolder.kt @@ -4,39 +4,41 @@ package org.mozilla.fenix.exceptions.viewholders -import android.view.View -import kotlinx.android.synthetic.main.exception_item.* +import androidx.recyclerview.widget.RecyclerView import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.ui.widgets.WidgetSiteItemView import org.mozilla.fenix.R import org.mozilla.fenix.exceptions.ExceptionsInteractor import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView -import org.mozilla.fenix.utils.view.ViewHolder /** * View holder for a single website that is exempted from Tracking Protection or Logins. */ class ExceptionsListItemViewHolder( - view: View, + private val view: WidgetSiteItemView, private val interactor: ExceptionsInteractor, private val icons: BrowserIcons = view.context.components.core.icons -) : ViewHolder(view) { +) : RecyclerView.ViewHolder(view) { private lateinit var item: T init { - delete_exception.setOnClickListener { + view.setSecondaryButton( + icon = R.drawable.ic_close, + contentDescription = R.string.history_delete_item + ) { interactor.onDeleteOne(item) } } fun bind(item: T, url: String) { this.item = item - webAddressView.text = url - icons.loadIntoView(favicon_image, url) + view.setText(label = url, caption = null) + icons.loadIntoView(view.iconView, url) } companion object { - const val LAYOUT_ID = R.layout.exception_item + const val LAYOUT_ID = R.layout.site_list_item } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt index 7dd2b3dce..e65ad9f35 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.ext import android.app.Activity import android.view.View import android.view.WindowManager +import mozilla.components.support.base.crash.Breadcrumb /** * Attempts to call immersive mode using the View to hide the status bar and navigation buttons. @@ -22,3 +23,19 @@ fun Activity.enterToImmersiveMode() { or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } + +fun Activity.breadcrumb( + message: String, + data: Map = emptyMap() +) { + components.analytics.crashReporter.recordCrashBreadcrumb( + Breadcrumb( + category = this::class.java.simpleName, + message = message, + data = data + mapOf( + "instance" to this.hashCode().toString() + ), + level = Breadcrumb.Level.INFO + ) + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/Context.kt b/app/src/main/java/org/mozilla/fenix/ext/Context.kt index 1e3d3b782..d10637026 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Context.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Context.kt @@ -6,6 +6,9 @@ package org.mozilla.fenix.ext import android.app.Activity import android.content.Context +import android.content.Intent +import android.os.Build +import android.provider.Settings import android.view.ContextThemeWrapper import android.view.View import android.view.ViewGroup @@ -89,3 +92,21 @@ fun Context.getStringWithArgSafe(@StringRes resId: Int, formatArg: String): Stri */ val Context.accessibilityManager: AccessibilityManager get() = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager + +/** + * Used to navigate to system notifications settings for app + */ +fun Context.navigateToNotificationsSettings() { + val intent = Intent() + intent.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + it.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS + it.putExtra(Settings.EXTRA_APP_PACKAGE, this.packageName) + } else { + it.action = "android.settings.APP_NOTIFICATION_SETTINGS" + it.putExtra("app_package", this.packageName) + it.putExtra("app_uid", this.applicationInfo.uid) + } + } + startActivity(intent) +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt b/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt new file mode 100644 index 000000000..93fa26aea --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/DownloadItem.kt @@ -0,0 +1,48 @@ +/* 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 org.mozilla.fenix.R +import org.mozilla.fenix.library.downloads.DownloadItem + +// While this looks complex, it's actually pretty simple. +@SuppressWarnings("ComplexMethod") +fun DownloadItem.getIcon(): Int { + fun getIconCornerCases(fileName: String?): Int { + return when { + fileName?.endsWith("apk") == true -> R.drawable.ic_file_type_apk + fileName?.endsWith("zip") == true -> R.drawable.ic_file_type_zip + else -> R.drawable.ic_file_type_default + } + } + + fun checkForApplicationArchiveSubtypes(contentType: String): Int? { + return when { + contentType.contains("rar") -> R.drawable.ic_file_type_zip + contentType.contains("zip") -> R.drawable.ic_file_type_zip + contentType.contains("7z") -> R.drawable.ic_file_type_zip + contentType.contains("tar") -> R.drawable.ic_file_type_zip + contentType.contains("freearc") -> R.drawable.ic_file_type_zip + contentType.contains("octet-stream") -> null + contentType.contains("vnd.android.package-archive") -> null + else -> R.drawable.ic_file_type_document + } + } + + fun getIconFromContentType(contentType: String): Int? { + return when { + contentType.contains("image/") -> R.drawable.ic_file_type_image + contentType.contains("audio/") -> R.drawable.ic_file_type_audio_note + contentType.contains("video/") -> R.drawable.ic_file_type_video + contentType.contains("application/") -> checkForApplicationArchiveSubtypes(contentType) + contentType.contains("text/") -> R.drawable.ic_file_type_document + else -> null + } + } + + return contentType?.let { contentType -> + getIconFromContentType(contentType) + } ?: getIconCornerCases(fileName) +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/EditText.kt b/app/src/main/java/org/mozilla/fenix/ext/EditText.kt new file mode 100644 index 000000000..ac7934b86 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/EditText.kt @@ -0,0 +1,14 @@ +/* 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.widget.EditText + +/** + * Places cursor at the end of an EditText. + */ +fun EditText.placeCursorAtEnd() { + this.text?.length?.let { setSelection(it, it) } +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt index f4128e335..39c3f7bb4 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt @@ -12,6 +12,7 @@ import androidx.navigation.NavDirections import androidx.navigation.NavOptions import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.findNavController +import mozilla.components.support.base.crash.Breadcrumb import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.Components @@ -57,3 +58,23 @@ fun Fragment.redirectToReAuth(destinations: List, currentDestination: Int?) findNavController().popBackStack(R.id.savedLoginsAuthFragment, false) } } + +fun Fragment.breadcrumb( + message: String, + data: Map = emptyMap() +) { + val activityName = activity?.let { it::class.java.simpleName } ?: "null" + + requireComponents.analytics.crashReporter.recordCrashBreadcrumb( + Breadcrumb( + category = this::class.java.simpleName, + message = message, + data = data + mapOf( + "instance" to hashCode().toString(), + "activityInstance" to activity?.hashCode().toString(), + "activityName" to activityName + ), + level = Breadcrumb.Level.INFO + ) + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/List.kt b/app/src/main/java/org/mozilla/fenix/ext/List.kt new file mode 100644 index 000000000..95510f66f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/ext/List.kt @@ -0,0 +1,20 @@ +/* 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 org.mozilla.fenix.library.downloads.DownloadItem +import java.io.File + +/** + * Checks a List of DownloadItems to verify whether items + * on that list are present on the disk or not. If a user has + * deleted the downloaded item it should not show on the downloaded + * list. + */ +fun List.filterNotExistsOnDisk(): List { + return this.filter { + File(it.filePath).exists() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/ext/LiveData.kt b/app/src/main/java/org/mozilla/fenix/ext/LiveData.kt deleted file mode 100644 index 718385219..000000000 --- a/app/src/main/java/org/mozilla/fenix/ext/LiveData.kt +++ /dev/null @@ -1,20 +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 androidx.lifecycle.LiveData -import androidx.lifecycle.Observer - -/** - * Observe a LiveData once and unregister from it as soon as the live data returns a value - */ -fun LiveData.observeOnce(observer: (T) -> Unit) { - observeForever(object : Observer { - override fun onChanged(value: T) { - removeObserver(this) - observer(value) - } - }) -} diff --git a/app/src/main/java/org/mozilla/fenix/ext/SpannableString.kt b/app/src/main/java/org/mozilla/fenix/ext/SpannableString.kt index 68a2b2e61..8a8de7dce 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/SpannableString.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/SpannableString.kt @@ -9,7 +9,7 @@ import android.text.Spannable import android.text.SpannableString import android.text.style.AbsoluteSizeSpan import android.text.style.ForegroundColorSpan -import androidx.core.content.ContextCompat +import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.util.dpToPx fun SpannableString.setTextSize(context: Context, textSize: Int) = @@ -23,10 +23,7 @@ fun SpannableString.setTextSize(context: Context, textSize: Int) = fun SpannableString.setTextColor(context: Context, colorResId: Int) = this.setSpan( ForegroundColorSpan( - ContextCompat.getColor( - context, - colorResId - ) + context.getColorFromAttr(colorResId) ), 0, this.length, 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 1becb7b8d..bcacb3ea6 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -16,7 +16,6 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.accessibility.AccessibilityEvent import android.widget.Button import android.widget.LinearLayout import android.widget.PopupWindow @@ -68,10 +67,13 @@ 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.lib.state.ext.consumeFrom +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions @@ -97,6 +99,7 @@ import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlView import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.DefaultTopSitesView import org.mozilla.fenix.onboarding.FenixOnboarding import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils.SumoTopic.HELP @@ -146,8 +149,11 @@ class HomeFragment : Fragment() { private val store: BrowserStore get() = requireComponents.core.store - private val onboarding by lazy { StrictMode.allowThreadDiskReads().resetPoliciesAfter { - FenixOnboarding(requireContext()) } } + private val onboarding by lazy { + StrictMode.allowThreadDiskReads().resetPoliciesAfter { + FenixOnboarding(requireContext()) + } + } private lateinit var homeFragmentStore: HomeFragmentStore private var _sessionControlInteractor: SessionControlInteractor? = null @@ -157,6 +163,8 @@ class HomeFragment : Fragment() { private var sessionControlView: SessionControlView? = null private lateinit var currentMode: CurrentMode + private val topSitesFeature = ViewBoundFeatureWrapper() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) postponeEnterTransition() @@ -190,22 +198,31 @@ class HomeFragment : Fragment() { collections = components.core.tabCollectionStorage.cachedTabCollections, expandedCollections = emptySet(), mode = currentMode.getCurrentMode(), - topSites = StrictMode.allowThreadDiskReads().resetPoliciesAfter { - components.core.topSiteStorage.cachedTopSites - }, - tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip() + topSites = components.core.topSiteStorage.cachedTopSites, + tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(), + showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome ) ) } + topSitesFeature.set( + feature = TopSitesFeature( + view = DefaultTopSitesView(homeFragmentStore), + storage = components.core.topSiteStorage, + config = ::getTopSitesConfig + ), + owner = this, + view = view + ) + _sessionControlInteractor = SessionControlInteractor( DefaultSessionControlController( activity = activity, + settings = components.settings, engine = components.core.engine, metrics = components.analytics.metrics, sessionManager = sessionManager, tabCollectionStorage = components.core.tabCollectionStorage, - topSiteStorage = components.core.topSiteStorage, addTabUseCase = components.useCases.tabsUseCases.addTab, fragmentStore = homeFragmentStore, navController = findNavController(), @@ -220,9 +237,9 @@ class HomeFragment : Fragment() { updateLayout(view) sessionControlView = SessionControlView( view.sessionControlRecyclerView, + viewLifecycleOwner, sessionControlInteractor, - homeViewModel, - requireComponents.core.store.state.normalTabs.isNotEmpty() + homeViewModel ) updateSessionControlView(view) @@ -231,6 +248,15 @@ class HomeFragment : Fragment() { return view } + /** + * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or + * not frequently visited sites should be displayed. + */ + private fun getTopSitesConfig(): TopSitesConfig { + val settings = requireContext().settings() + return TopSitesConfig(settings.topSitesMaxLimit, settings.showTopFrecentSites) + } + /** * 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 @@ -344,7 +370,7 @@ class HomeFragment : Fragment() { view.toolbar_wrapper.setOnLongClickListener { ToolbarPopupWindow.show( - WeakReference(view), + WeakReference(it), handlePasteAndGo = sessionControlInteractor::onPasteAndGo, handlePaste = sessionControlInteractor::onPaste, copyVisible = false @@ -374,7 +400,8 @@ class HomeFragment : Fragment() { // We call this onLayout so that the bottom bar width is correctly set for us to center // the CFR in. view.toolbar_wrapper.doOnLayout { - if (!browsingModeManager.mode.isPrivate) { + val willNavigateToSearch = !bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience + if (!browsingModeManager.mode.isPrivate && !willNavigateToSearch) { SearchWidgetCFR( context = view.context, settings = view.context.settings(), @@ -385,19 +412,6 @@ class HomeFragment : Fragment() { } } - val args by navArgs() - - if (view.context.settings().accessibilityServicesEnabled && - args.focusOnAddressBar - ) { - // We cannot put this in the fragment_home.xml file as it breaks tests - view.toolbar_wrapper.isFocusableInTouchMode = true - viewLifecycleOwner.lifecycleScope.launch { - view.toolbar_wrapper?.requestFocus() - view.toolbar_wrapper?.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) - } - } - if (browsingModeManager.mode.isPrivate) { requireActivity().window.addFlags(FLAG_SECURE) } else { @@ -418,7 +432,7 @@ class HomeFragment : Fragment() { updateTabCounter(requireComponents.core.store.state) - if (args.focusOnAddressBar && requireContext().settings().useNewSearchExperience) { + if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) && FeatureFlags.newSearchExperience) { navigateToSearch() } } @@ -506,7 +520,6 @@ class HomeFragment : Fragment() { override fun onStart() { super.onStart() subscribeToTabCollections() - subscribeToTopSites() val context = requireContext() val components = context.components @@ -516,7 +529,8 @@ class HomeFragment : Fragment() { collections = components.core.tabCollectionStorage.cachedTabCollections, mode = currentMode.getCurrentMode(), topSites = components.core.topSiteStorage.cachedTopSites, - tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip() + tip = FenixTipManager(listOf(MigrationTipProvider(requireContext()))).getTip(), + showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome ) ) @@ -556,6 +570,10 @@ class HomeFragment : Fragment() { // We only want this observer live just before we navigate away to the collection creation screen requireComponents.core.tabCollectionStorage.unregister(collectionStorageObserver) + + lifecycleScope.launch(IO) { + requireComponents.reviewPromptController.promptReview(requireActivity()) + } } private fun dispatchModeChanges(mode: Mode) { @@ -679,7 +697,7 @@ class HomeFragment : Fragment() { } private fun navigateToSearch() { - val directions = if (requireContext().settings().useNewSearchExperience) { + val directions = if (FeatureFlags.newSearchExperience) { HomeFragmentDirections.actionGlobalSearchDialog( sessionId = null ) @@ -771,6 +789,15 @@ class HomeFragment : Fragment() { HomeFragmentDirections.actionGlobalHistoryFragment() ) } + + HomeMenu.Item.Downloads -> { + hideOnboardingIfNeeded() + nav( + R.id.homeFragment, + HomeFragmentDirections.actionGlobalDownloadsFragment() + ) + } + HomeMenu.Item.Help -> { hideOnboardingIfNeeded() (activity as HomeActivity).openToBrowserAndLoad( @@ -832,17 +859,6 @@ class HomeFragment : Fragment() { } } - private fun subscribeToTopSites(): Observer> { - return Observer> { topSites -> - requireComponents.core.topSiteStorage.cachedTopSites = topSites - context?.settings()?.preferences?.edit() - ?.putInt(getString(R.string.pref_key_top_sites_size), topSites.size)?.apply() - homeFragmentStore.dispatch(HomeFragmentAction.TopSitesChange(topSites)) - }.also { observer -> - requireComponents.core.topSiteStorage.getTopSites().observe(this, observer) - } - } - private fun registerCollectionStorageObserver() { requireComponents.core.tabCollectionStorage.register(collectionStorageObserver, this) } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt index bbe46024a..dc6b78d7d 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -41,13 +41,16 @@ data class Tab( * @property mode The state of the [HomeFragment] UI. * @property tabs The list of opened [Tab] in the [HomeFragment]. * @property topSites The list of [TopSite] in the [HomeFragment]. + * @property tip The current [Tip] to show on the [HomeFragment]. + * @property showCollectionPlaceholder If true, shows a placeholder when there are no collections. */ data class HomeFragmentState( val collections: List, val expandedCollections: Set, val mode: Mode, val topSites: List, - val tip: Tip? = null + val tip: Tip? = null, + val showCollectionPlaceholder: Boolean ) : State sealed class HomeFragmentAction : Action { @@ -55,7 +58,8 @@ sealed class HomeFragmentAction : Action { val topSites: List, val mode: Mode, val collections: List, - val tip: Tip? = null + val tip: Tip? = null, + val showCollectionPlaceholder: Boolean ) : HomeFragmentAction() @@ -66,6 +70,7 @@ sealed class HomeFragmentAction : Action { data class ModeChange(val mode: Mode) : HomeFragmentAction() data class TopSitesChange(val topSites: List) : HomeFragmentAction() data class RemoveTip(val tip: Tip) : HomeFragmentAction() + object RemoveCollectionsPlaceholder : HomeFragmentAction() } private fun homeFragmentStateReducer( @@ -93,6 +98,11 @@ private fun homeFragmentStateReducer( is HomeFragmentAction.CollectionsChange -> state.copy(collections = action.collections) is HomeFragmentAction.ModeChange -> state.copy(mode = action.mode) is HomeFragmentAction.TopSitesChange -> state.copy(topSites = action.topSites) - is HomeFragmentAction.RemoveTip -> { state.copy(tip = null) } + is HomeFragmentAction.RemoveTip -> { + state.copy(tip = null) + } + is HomeFragmentAction.RemoveCollectionsPlaceholder -> { + state.copy(showCollectionPlaceholder = false) + } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt index 1ab68040b..579e4bf95 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -43,6 +43,7 @@ class HomeMenu( object SyncedTabs : Item() object History : Item() object Bookmarks : Item() + object Downloads : Item() object Quit : Item() object Sync : Item() } @@ -144,6 +145,14 @@ class HomeMenu( onItemTapped.invoke(Item.Help) } + val downloadsItem = BrowserMenuImageText( + "Downloads", + R.drawable.ic_download, + primaryTextColor + ) { + onItemTapped.invoke(Item.Downloads) + } + // Only query account manager if it has been initialized. // We don't want to cause its initialization just for this check. val accountAuthItem = if (context.components.backgroundServices.accountManagerAvailableQueue.isReady()) { @@ -158,9 +167,10 @@ class HomeMenu( if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null, settingsItem, BrowserMenuDivider(), - if (FeatureFlags.syncedTabs) syncedTabsItem else null, + if (settings.syncedTabsInTabsTray) null else syncedTabsItem, bookmarksItem, historyItem, + if (FeatureFlags.viewDownloads) downloadsItem else null, BrowserMenuDivider(), addons, BrowserMenuDivider(), 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 6acca7d2e..bde850a17 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 @@ -8,11 +8,13 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.annotation.LayoutRes +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.home.OnboardingState import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionHeaderViewHolder @@ -20,7 +22,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder -import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder @@ -40,14 +42,14 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { ButtonTipViewHolder.LAYOUT_ID ) - data class TopSiteList(val topSites: List) : AdapterItem(TopSiteViewHolder.LAYOUT_ID) { + data class TopSitePager(val topSites: List) : AdapterItem(TopSitePagerViewHolder.LAYOUT_ID) { override fun sameAs(other: AdapterItem): Boolean { - val newTopSites = (other as? TopSiteList) ?: return false + val newTopSites = (other as? TopSitePager) ?: return false return newTopSites.topSites == this.topSites } override fun contentsSameAs(other: AdapterItem): Boolean { - val newTopSites = (other as? TopSiteList) ?: return false + val newTopSites = (other as? TopSitePager) ?: return false if (newTopSites.topSites.size != this.topSites.size) return false val newSitesSequence = newTopSites.topSites.asSequence() val oldTopSites = this.topSites.asSequence() @@ -135,7 +137,8 @@ class AdapterItemDiffCallback : DiffUtil.ItemCallback() { class SessionControlAdapter( private val interactor: SessionControlInteractor, - private val hasNormalTabsOpened: Boolean + private val viewLifecycleOwner: LifecycleOwner, + private val components: Components ) : ListAdapter(AdapterItemDiffCallback()) { // This method triggers the ComplexMethod lint error when in fact it's quite simple. @@ -144,13 +147,18 @@ class SessionControlAdapter( val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) return when (viewType) { ButtonTipViewHolder.LAYOUT_ID -> ButtonTipViewHolder(view, interactor) - TopSiteViewHolder.LAYOUT_ID -> TopSiteViewHolder(view, interactor) + TopSitePagerViewHolder.LAYOUT_ID -> TopSitePagerViewHolder(view, interactor) PrivateBrowsingDescriptionViewHolder.LAYOUT_ID -> PrivateBrowsingDescriptionViewHolder( view, interactor ) NoCollectionsMessageViewHolder.LAYOUT_ID -> - NoCollectionsMessageViewHolder(view, interactor, hasNormalTabsOpened) + NoCollectionsMessageViewHolder( + view, + viewLifecycleOwner, + components.core.store, + interactor + ) CollectionHeaderViewHolder.LAYOUT_ID -> CollectionHeaderViewHolder(view) CollectionViewHolder.LAYOUT_ID -> CollectionViewHolder(view, interactor) TabInCollectionViewHolder.LAYOUT_ID -> TabInCollectionViewHolder( @@ -195,8 +203,8 @@ class SessionControlAdapter( val tipItem = item as AdapterItem.TipItem holder.bind(tipItem.tip) } - is TopSiteViewHolder -> { - holder.bind((item as AdapterItem.TopSiteList).topSites) + is TopSitePagerViewHolder -> { + holder.bind((item as AdapterItem.TopSitePager).topSites) } is CollectionViewHolder -> { val (collection, expanded) = item as AdapterItem.CollectionItem 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 a5b4b7de7..7c927d2c5 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 @@ -22,7 +22,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.components.TopSiteStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils @@ -37,6 +36,7 @@ import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentStore import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.utils.Settings import mozilla.components.feature.tab.collections.Tab as ComponentTab /** @@ -144,16 +144,21 @@ interface SessionControlController { * @see [CollectionInteractor.onAddTabsToCollectionTapped] */ fun handleCreateCollection() + + /** + * @see [CollectionInteractor.onRemoveCollectionsPlaceholder] + */ + fun handleRemoveCollectionsPlaceholder() } @Suppress("TooManyFunctions", "LargeClass") class DefaultSessionControlController( private val activity: HomeActivity, + private val settings: Settings, private val engine: Engine, private val metrics: MetricController, private val sessionManager: SessionManager, private val tabCollectionStorage: TabCollectionStorage, - private val topSiteStorage: TopSiteStorage, private val addTabUseCase: TabsUseCases.AddNewTabUseCase, private val fragmentStore: HomeFragmentStore, private val navController: NavController, @@ -213,7 +218,11 @@ class DefaultSessionControlController( metrics.track(Event.CollectionAllTabsRestored) } - override fun handleCollectionRemoveTab(collection: TabCollection, tab: ComponentTab, wasSwiped: Boolean) { + override fun handleCollectionRemoveTab( + collection: TabCollection, + tab: ComponentTab, + wasSwiped: Boolean + ) { metrics.track(Event.CollectionTabRemoved) if (collection.tabs.size == 1) { @@ -223,7 +232,13 @@ class DefaultSessionControlController( ) val message = activity.resources.getString(R.string.delete_tab_and_collection_dialog_message) - showDeleteCollectionPrompt(collection, title, message, wasSwiped, handleSwipedItemDeletionCancel) + showDeleteCollectionPrompt( + collection, + title, + message, + wasSwiped, + handleSwipedItemDeletionCancel + ) } else { viewLifecycleScope.launch(Dispatchers.IO) { tabCollectionStorage.removeTabFromCollection(collection, tab) @@ -273,7 +288,9 @@ class DefaultSessionControlController( } viewLifecycleScope.launch(Dispatchers.IO) { - topSiteStorage.removeTopSite(topSite) + with(activity.components.useCases.topSitesUseCase) { + removeTopSites(topSite) + } } } @@ -369,6 +386,11 @@ class DefaultSessionControlController( showTabTrayCollectionCreation() } + override fun handleRemoveCollectionsPlaceholder() { + settings.showCollectionsPlaceholderOnHome = false + fragmentStore.dispatch(HomeFragmentAction.RemoveCollectionsPlaceholder) + } + private fun showShareFragment(shareSubject: String, data: List) { val directions = HomeFragmentDirections.actionGlobalShareFragment( shareSubject = shareSubject, diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt index 644178f38..b33b97699 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlInteractor.kt @@ -93,6 +93,11 @@ interface CollectionInteractor { * Opens the collection creator */ fun onAddTabsToCollectionTapped() + + /** + * User has removed the collections placeholder from home. + */ + fun onRemoveCollectionsPlaceholder() } interface ToolbarInteractor { @@ -256,4 +261,8 @@ class SessionControlInteractor( override fun onPaste(clipboardText: String) { controller.handlePaste(clipboardText) } + + override fun onRemoveCollectionsPlaceholder() { + controller.handleRemoveCollectionsPlaceholder() + } } 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 c2f363140..5aaaa3854 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 @@ -5,6 +5,7 @@ package org.mozilla.fenix.home.sessioncontrol import android.view.View +import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -13,6 +14,7 @@ import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.R import org.mozilla.fenix.components.tips.Tip +import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.Mode @@ -25,19 +27,21 @@ private fun normalModeAdapterItems( topSites: List, collections: List, expandedCollections: Set, - tip: Tip? + tip: Tip?, + showCollectionsPlaceholder: Boolean ): List { val items = mutableListOf() tip?.let { items.add(AdapterItem.TipItem(it)) } if (topSites.isNotEmpty()) { - items.add(AdapterItem.TopSiteList(topSites)) + items.add(AdapterItem.TopSitePager(topSites)) } if (collections.isEmpty()) { - items.add(AdapterItem.CollectionHeader) - items.add(AdapterItem.NoCollectionsMessage) + if (showCollectionsPlaceholder) { + items.add(AdapterItem.NoCollectionsMessage) + } } else { showCollections(collections, expandedCollections, items) } @@ -68,62 +72,77 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List = mutableListOf(AdapterItem.OnboardingHeader) // Customize FxA items based on where we are with the account state: - items.addAll(when (onboardingState) { - OnboardingState.SignedOutNoAutoSignIn -> { - listOf( - AdapterItem.OnboardingManualSignIn - ) - } - is OnboardingState.SignedOutCanAutoSignIn -> { - listOf( - AdapterItem.OnboardingAutomaticSignIn(onboardingState) - ) + items.addAll( + when (onboardingState) { + OnboardingState.SignedOutNoAutoSignIn -> { + listOf( + AdapterItem.OnboardingManualSignIn + ) + } + is OnboardingState.SignedOutCanAutoSignIn -> { + listOf( + AdapterItem.OnboardingAutomaticSignIn(onboardingState) + ) + } + OnboardingState.SignedIn -> listOf() } - OnboardingState.SignedIn -> listOf() - }) - - items.addAll(listOf( - AdapterItem.OnboardingSectionHeader { - val appName = it.getString(R.string.app_name) - it.getString(R.string.onboarding_feature_section_header, appName) - }, - AdapterItem.OnboardingWhatsNew, - AdapterItem.OnboardingTrackingProtection, - AdapterItem.OnboardingThemePicker, - AdapterItem.OnboardingPrivateBrowsing, - AdapterItem.OnboardingToolbarPositionPicker, - AdapterItem.OnboardingPrivacyNotice, - AdapterItem.OnboardingFinish - )) + ) + + items.addAll( + listOf( + AdapterItem.OnboardingSectionHeader { + val appName = it.getString(R.string.app_name) + it.getString(R.string.onboarding_feature_section_header, appName) + }, + AdapterItem.OnboardingWhatsNew, + AdapterItem.OnboardingTrackingProtection, + AdapterItem.OnboardingThemePicker, + AdapterItem.OnboardingPrivateBrowsing, + AdapterItem.OnboardingToolbarPositionPicker, + AdapterItem.OnboardingPrivacyNotice, + AdapterItem.OnboardingFinish + ) + ) return items } private fun HomeFragmentState.toAdapterList(): List = when (mode) { - is Mode.Normal -> normalModeAdapterItems(topSites, collections, expandedCollections, tip) + is Mode.Normal -> normalModeAdapterItems( + topSites, + collections, + expandedCollections, + tip, + showCollectionPlaceholder + ) is Mode.Private -> privateModeAdapterItems() is Mode.Onboarding -> onboardingAdapterItems(mode.state) } -private fun collectionTabItems(collection: TabCollection) = collection.tabs.mapIndexed { index, tab -> +private fun collectionTabItems(collection: TabCollection) = + collection.tabs.mapIndexed { index, tab -> AdapterItem.TabInCollectionItem(collection, tab, index == collection.tabs.lastIndex) -} + } class SessionControlView( - override val containerView: View?, + override val containerView: View, + viewLifecycleOwner: LifecycleOwner, interactor: SessionControlInteractor, - private var homeScreenViewModel: HomeScreenViewModel, - private val hasNormalTabsOpened: Boolean + private var homeScreenViewModel: HomeScreenViewModel ) : LayoutContainer { val view: RecyclerView = containerView as RecyclerView - private val sessionControlAdapter = SessionControlAdapter(interactor, hasNormalTabsOpened) + private val sessionControlAdapter = SessionControlAdapter( + interactor, + viewLifecycleOwner, + containerView.context.components + ) init { view.apply { adapter = sessionControlAdapter - layoutManager = LinearLayoutManager(containerView!!.context) + layoutManager = LinearLayoutManager(containerView.context) val itemTouchHelper = ItemTouchHelper( SwipeToDeleteCallback( @@ -141,7 +160,7 @@ class SessionControlView( sessionControlAdapter.submitList(stateAdapterList) { val loadedTopSites = stateAdapterList.find { adapterItem -> - adapterItem is AdapterItem.TopSiteList && adapterItem.topSites.isNotEmpty() + adapterItem is AdapterItem.TopSitePager && adapterItem.topSites.isNotEmpty() } loadedTopSites?.run { homeScreenViewModel.shouldScrollToTopSites = false 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 00a42acdc..ed3c128d0 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 @@ -6,22 +6,50 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders import android.view.View import androidx.core.view.isVisible +import androidx.lifecycle.LifecycleOwner import kotlinx.android.synthetic.main.no_collections_message.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +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.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R -import org.mozilla.fenix.utils.view.ViewHolder +import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.home.sessioncontrol.CollectionInteractor +import org.mozilla.fenix.utils.view.ViewHolder +@OptIn(ExperimentalCoroutinesApi::class) open class NoCollectionsMessageViewHolder( view: View, - interactor: CollectionInteractor, - hasNormalTabsOpened: Boolean + viewLifecycleOwner: LifecycleOwner, + store: BrowserStore, + interactor: CollectionInteractor ) : ViewHolder(view) { init { add_tabs_to_collections_button.setOnClickListener { interactor.onAddTabsToCollectionTapped() } - add_tabs_to_collections_button.isVisible = hasNormalTabsOpened + + remove_collection_placeholder.increaseTapArea( + view.resources.getDimensionPixelSize(R.dimen.tap_increase_16) + ) + + remove_collection_placeholder.setOnClickListener { + interactor.onRemoveCollectionsPlaceholder() + } + + add_tabs_to_collections_button.isVisible = store.state.normalTabs.isNotEmpty() + + store.flowScoped(viewLifecycleOwner) { flow -> + flow.map { state -> state.normalTabs.size } + .ifChanged() + .collect { tabs -> + add_tabs_to_collections_button.isVisible = tabs > 0 + } + } } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt new file mode 100644 index 000000000..ab0142fa1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.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.home.sessioncontrol.viewholders + +import android.view.View +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 +import kotlinx.android.synthetic.main.component_top_sites_pager.view.* +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.R +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor +import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesPagerAdapter + +class TopSitePagerViewHolder( + view: View, + interactor: TopSiteInteractor +) : RecyclerView.ViewHolder(view) { + + private val topSitesPagerAdapter = TopSitesPagerAdapter(interactor) + private val pageIndicator = view.page_indicator + + private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + pageIndicator.setSelection(position) + } + } + + init { + view.top_sites_pager.apply { + adapter = topSitesPagerAdapter + registerOnPageChangeCallback(topSitesPageChangeCallback) + } + } + + fun bind(topSites: List) { + topSitesPagerAdapter.updateData(topSites) + + // Don't show any page indicator if there is only 1 page. + val numPages = if (topSites.size > TOP_SITES_PER_PAGE) { + TOP_SITES_MAX_PAGE_SIZE + } else { + 0 + } + + pageIndicator.isVisible = numPages > 1 + pageIndicator.setSize(numPages) + } + + companion object { + const val LAYOUT_ID = R.layout.component_top_sites_pager + const val TOP_SITES_MAX_PAGE_SIZE = 2 + const val TOP_SITES_PER_PAGE = 8 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt index 44359036e..7f1d9b91e 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSiteViewHolder.kt @@ -6,7 +6,6 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders import android.view.View import androidx.recyclerview.widget.RecyclerView -import com.google.android.flexbox.FlexboxLayoutManager import kotlinx.android.synthetic.main.component_top_sites.view.* import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.R @@ -23,8 +22,6 @@ class TopSiteViewHolder( init { view.top_sites_list.apply { adapter = topSitesAdapter - layoutManager = FlexboxLayoutManager(view.context) - isNestedScrollingEnabled = false } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt index 39c49a414..c09fb31d2 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingAutomaticSignInViewHolder.kt @@ -31,9 +31,9 @@ class OnboardingAutomaticSignInViewHolder( private val headerText = view.header_text init { - view.turn_on_sync_button.setOnClickListener { + view.fxa_sign_in_button.setOnClickListener { scope.launch { - onClick(it.turn_on_sync_button) + onClick(it.fxa_sign_in_button) } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt index e4fa6d5a3..98155f061 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingManualSignInViewHolder.kt @@ -5,40 +5,40 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding import android.view.View -import androidx.core.content.ContextCompat import androidx.navigation.Navigation import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.onboarding_manual_signin.view.* -import mozilla.components.support.ktx.android.content.getDrawableWithTint -import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.addUnderline import org.mozilla.fenix.ext.components import org.mozilla.fenix.home.HomeFragmentDirections +import org.mozilla.fenix.onboarding.OnboardingController +import org.mozilla.fenix.onboarding.OnboardingInteractor class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) { private val headerText = view.header_text init { - view.turn_on_sync_button.setOnClickListener { + val interactor = OnboardingInteractor(OnboardingController(itemView.context)) + + view.fxa_sign_in_button.setOnClickListener { it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn) val directions = HomeFragmentDirections.actionGlobalTurnOnSync() Navigation.findNavController(view).navigate(directions) } + + view.learn_more.addUnderline() + view.learn_more.setOnClickListener { + interactor.onLearnMoreClicked() + } } fun bind() { val context = itemView.context - - val appName = context.getString(R.string.app_name) - headerText.text = context.getString(R.string.onboarding_firefox_account_header, appName) - val icon = context.getDrawableWithTint( - R.drawable.ic_onboarding_firefox_accounts, - ContextCompat.getColor(context, R.color.white_color) - ) - headerText.putCompoundDrawablesRelativeWithIntrinsicBounds(start = icon) + headerText.text = context.getString(R.string.onboarding_account_sign_in_header) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/DefaultTopSitesView.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/DefaultTopSitesView.kt new file mode 100644 index 000000000..63fd9c2c9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/DefaultTopSitesView.kt @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites + +import mozilla.components.feature.top.sites.TopSite +import mozilla.components.feature.top.sites.view.TopSitesView +import org.mozilla.fenix.home.HomeFragmentAction +import org.mozilla.fenix.home.HomeFragmentStore + +class DefaultTopSitesView( + val store: HomeFragmentStore +) : TopSitesView { + + override fun displayTopSites(topSites: List) { + store.dispatch(HomeFragmentAction.TopSitesChange(topSites)) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/PagerIndicator.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/PagerIndicator.kt new file mode 100644 index 000000000..dd5acdf5e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/PagerIndicator.kt @@ -0,0 +1,82 @@ +/* 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.sessioncontrol.viewholders.topsites + +import android.content.Context +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.MarginLayoutParamsCompat +import org.mozilla.fenix.R + +/** + * A pager indicator widget to display the number of pages and the current selected page. + */ +class PagerIndicator : LinearLayout { + + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + private var selectedIndex = 0 + + /** + * Set the number of pager dots to display. + */ + fun setSize(size: Int) { + if (childCount == size) { + return + } + if (selectedIndex >= size) { + selectedIndex = size - 1 + } + + removeAllViews() + for (i in 0 until size) { + val isLast = i == size - 1 + addView( + View(context).apply { + setBackgroundResource(R.drawable.pager_dot) + isSelected = i == selectedIndex + }, + LayoutParams(dpToPx(DOT_SIZE_IN_DP), dpToPx(DOT_SIZE_IN_DP)).apply { + if (!isLast) { + MarginLayoutParamsCompat.setMarginEnd(this, dpToPx(DOT_MARGIN)) + } + } + ) + } + } + + /** + * Set the current selected pager dot. + */ + fun setSelection(index: Int) { + if (selectedIndex == index) { + return + } + + getChildAt(selectedIndex)?.run { + isSelected = false + } + getChildAt(index)?.run { + isSelected = true + } + selectedIndex = index + } + + companion object { + private const val DOT_SIZE_IN_DP = 6f + private const val DOT_MARGIN = 4f + } +} + +fun Context.dpToPx(value: Float): Int = + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics).toInt() + +fun View.dpToPx(value: Float): Int = context.dpToPx(value) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt index 4f5ee6e1e..af5bb1de5 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSiteItemViewHolder.kt @@ -14,6 +14,8 @@ import kotlinx.android.synthetic.main.top_site_item.* import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.feature.top.sites.TopSite +import mozilla.components.feature.top.sites.TopSite.Type.DEFAULT +import mozilla.components.feature.top.sites.TopSite.Type.FRECENT import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView @@ -26,23 +28,23 @@ class TopSiteItemViewHolder( private val interactor: TopSiteInteractor ) : ViewHolder(view) { private lateinit var topSite: TopSite - private var topSiteMenu: TopSiteItemMenu init { - topSiteMenu = TopSiteItemMenu(view.context) { - when (it) { - is TopSiteItemMenu.Item.OpenInPrivateTab -> interactor.onOpenInPrivateTabClicked( - topSite - ) - is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked(topSite) - } - } - top_site_item.setOnClickListener { - interactor.onSelectTopSite(topSite.url, topSite.isDefault) + interactor.onSelectTopSite(topSite.url, topSite.type === DEFAULT) } top_site_item.setOnLongClickListener { + val topSiteMenu = TopSiteItemMenu(view.context, topSite.type != FRECENT) { item -> + when (item) { + is TopSiteItemMenu.Item.OpenInPrivateTab -> interactor.onOpenInPrivateTabClicked( + topSite + ) + is TopSiteItemMenu.Item.RemoveTopSite -> interactor.onRemoveTopSiteClicked( + topSite + ) + } + } val menu = topSiteMenu.menuBuilder.build(view.context).show(anchor = it) it.setOnTouchListener @SuppressLint("ClickableViewAccessibility") { v, event -> onTouchEvent(v, event, menu) @@ -82,6 +84,7 @@ class TopSiteItemViewHolder( class TopSiteItemMenu( private val context: Context, + private val isPinnedSite: Boolean, private val onItemTapped: (Item) -> Unit = {} ) { sealed class Item { @@ -98,9 +101,12 @@ class TopSiteItemMenu( ) { onItemTapped.invoke(Item.OpenInPrivateTab) }, - SimpleBrowserMenuItem( - context.getString(R.string.remove_top_site) + if (isPinnedSite) { + context.getString(R.string.remove_top_site) + } else { + context.getString(R.string.delete_from_history) + } ) { onItemTapped.invoke(Item.RemoveTopSite) } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt new file mode 100644 index 000000000..b335e57de --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt @@ -0,0 +1,40 @@ +/* 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.sessioncontrol.viewholders.topsites + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.feature.top.sites.TopSite +import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor +import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder + +class TopSitesPagerAdapter( + private val interactor: TopSiteInteractor +) : RecyclerView.Adapter() { + + private var topSites: List> = listOf() + + fun updateData(topSites: List) { + this.topSites = topSites.chunked(TOP_SITES_PER_PAGE) + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(TopSiteViewHolder.LAYOUT_ID, parent, false) + return TopSiteViewHolder(view, interactor) + } + + override fun onBindViewHolder(holder: TopSiteViewHolder, position: Int) { + holder.bind(this.topSites[position]) + } + + override fun getItemCount(): Int = this.topSites.size + + companion object { + const val TOP_SITES_PER_PAGE = 8 + } +} 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 a578b898f..27ecc25d4 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 @@ -21,6 +21,7 @@ import androidx.navigation.Navigation import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import kotlinx.android.synthetic.main.fragment_edit_bookmark.* +import kotlinx.android.synthetic.main.fragment_edit_bookmark.view.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch @@ -31,6 +32,7 @@ import mozilla.components.concept.storage.BookmarkNode import mozilla.components.concept.storage.BookmarkNodeType import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.view.hideKeyboard +import mozilla.components.support.ktx.android.view.showKeyboard import org.mozilla.fenix.NavHostActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar @@ -38,6 +40,7 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.placeCursorAtEnd import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.setToolbarColors import org.mozilla.fenix.ext.toShortUrl @@ -107,6 +110,12 @@ class EditBookmarkFragment : Fragment(R.layout.fragment_edit_bookmark) { ) } } + + view.bookmarkNameEdit.apply { + requestFocus() + placeCursorAtEnd() + showKeyboard() + } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt index ad9aca6e9..d9064ef29 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/selectfolder/SelectBookmarkFolderAdapter.kt @@ -8,6 +8,7 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.ContextCompat +import androidx.core.view.updatePaddingRelative import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -86,7 +87,7 @@ class SelectBookmarkFolderAdapter(private val sharedViewModel: BookmarksSharedVi } val pxToIndent = dpsToIndent.dpToPx(view.context.resources.displayMetrics) val padding = pxToIndent * if (folder.depth > maxDepth) maxDepth else folder.depth - view.setPadding(padding, 0, 0, 0) + view.updatePaddingRelative(start = padding) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadAdapter.kt new file mode 100644 index 000000000..71fc6d0f4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadAdapter.kt @@ -0,0 +1,40 @@ +/* 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.library.downloads + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder + +class DownloadAdapter( + private val downloadInteractor: DownloadInteractor +) : RecyclerView.Adapter(), SelectionHolder { + private var downloads: List = listOf() + private var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal + override val selectedItems get() = mode.selectedItems + + override fun getItemCount(): Int = downloads.size + override fun getItemViewType(position: Int): Int = DownloadsListItemViewHolder.LAYOUT_ID + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadsListItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + return DownloadsListItemViewHolder(view, downloadInteractor, this) + } + + fun updateMode(mode: DownloadFragmentState.Mode) { + this.mode = mode + } + + override fun onBindViewHolder(holder: DownloadsListItemViewHolder, position: Int) { + holder.bind(downloads[position]) + } + + fun updateDownloads(downloads: List) { + this.downloads = downloads + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadController.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadController.kt new file mode 100644 index 000000000..ccd30c136 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadController.kt @@ -0,0 +1,30 @@ +/* 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.library.downloads + +import org.mozilla.fenix.browser.browsingmode.BrowsingMode + +interface DownloadController { + fun handleOpen(item: DownloadItem, mode: BrowsingMode? = null) + fun handleBackPressed(): Boolean +} + +class DefaultDownloadController( + private val store: DownloadFragmentStore, + private val openToFileManager: (item: DownloadItem, mode: BrowsingMode?) -> Unit +) : DownloadController { + override fun handleOpen(item: DownloadItem, mode: BrowsingMode?) { + openToFileManager(item, mode) + } + + override fun handleBackPressed(): Boolean { + return if (store.state.mode is DownloadFragmentState.Mode.Editing) { + store.dispatch(DownloadFragmentAction.ExitEditMode) + true + } else { + false + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt new file mode 100644 index 000000000..95effa563 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt @@ -0,0 +1,113 @@ +/* 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.library.downloads + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import kotlinx.android.synthetic.main.fragment_downloads.view.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.lib.state.ext.consumeFrom +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.filterNotExistsOnDisk +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.library.LibraryPageFragment + +@SuppressWarnings("TooManyFunctions", "LargeClass") +class DownloadFragment : LibraryPageFragment(), UserInteractionHandler { + private lateinit var downloadStore: DownloadFragmentStore + private lateinit var downloadView: DownloadView + private lateinit var downloadInteractor: DownloadInteractor + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_downloads, container, false) + + val items = requireComponents.core.store.state.downloads.map { + DownloadItem( + it.value.id.toString(), + it.value.fileName, + it.value.filePath, + it.value.contentLength.toString(), + it.value.contentType, + it.value.status + ) + }.filter { + it.status == DownloadState.Status.COMPLETED + }.filterNotExistsOnDisk() + + downloadStore = StoreProvider.get(this) { + DownloadFragmentStore( + DownloadFragmentState( + items = items, + mode = DownloadFragmentState.Mode.Normal + ) + ) + } + + val downloadController: DownloadController = DefaultDownloadController( + downloadStore, + ::openItem + ) + downloadInteractor = DownloadInteractor( + downloadController + ) + downloadView = DownloadView(view.downloadsLayout, downloadInteractor) + + return view + } + + override val selectedItems get() = downloadStore.state.mode.selectedItems + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + requireComponents.analytics.metrics.track(Event.HistoryOpened) + + setHasOptionsMenu(false) + } + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + consumeFrom(downloadStore) { + downloadView.update(it) + } + } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.library_downloads)) + } + + override fun onBackPressed(): Boolean { + return downloadView.onBackPressed() + } + + private fun openItem(item: DownloadItem, mode: BrowsingMode? = null) { + + mode?.let { (activity as HomeActivity).browsingModeManager.mode = it } + context?.let { + AbstractFetchDownloadService.openFile( + context = it, + contentType = item.contentType, + filePath = item.filePath + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.kt new file mode 100644 index 000000000..8f4915e33 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragmentStore.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.library.downloads + +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * Class representing a history entry + * @property id Unique id of the download item + * @property fileName File name of the download item + * @property filePath Full path of the download item + * @property size The size in bytes of the download item + * @property contentType The type of file the download is + */ +data class DownloadItem( + val id: String, + val fileName: String?, + val filePath: String, + val size: String, + val contentType: String?, + val status: DownloadState.Status +) + +/** + * The [Store] for holding the [DownloadFragmentState] and applying [DownloadFragmentAction]s. + */ +class DownloadFragmentStore(initialState: DownloadFragmentState) : + Store(initialState, ::downloadStateReducer) + +/** + * Actions to dispatch through the `DownloadStore` to modify `DownloadState` through the reducer. + */ +sealed class DownloadFragmentAction : Action { + object ExitEditMode : DownloadFragmentAction() +} + +/** + * The state for the Download Screen + * @property items List of DownloadItem to display + * @property mode Current Mode of Download + */ +data class DownloadFragmentState( + val items: List, + val mode: Mode +) : State { + sealed class Mode { + open val selectedItems = emptySet() + + object Normal : Mode() + data class Editing(override val selectedItems: Set) : DownloadFragmentState.Mode() + } +} + +/** + * The DownloadState Reducer. + */ +private fun downloadStateReducer( + state: DownloadFragmentState, + action: DownloadFragmentAction +): DownloadFragmentState { + return when (action) { + is DownloadFragmentAction.ExitEditMode -> state.copy(mode = DownloadFragmentState.Mode.Normal) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadInteractor.kt new file mode 100644 index 000000000..ea55bd2eb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadInteractor.kt @@ -0,0 +1,25 @@ +/* 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.library.downloads +/** + * Interactor for the download screen + * Provides implementations for the DownloadViewInteractor + */ +@SuppressWarnings("TooManyFunctions") +class DownloadInteractor( + private val downloadController: DownloadController +) : DownloadViewInteractor { + override fun open(item: DownloadItem) { + downloadController.handleOpen(item) + } + + override fun select(item: DownloadItem) { /* noop */ } + + override fun deselect(item: DownloadItem) { /* noop */ } + + override fun onBackPressed(): Boolean { + return downloadController.handleBackPressed() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadView.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadView.kt new file mode 100644 index 000000000..76989458d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadView.kt @@ -0,0 +1,83 @@ +/* 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.library.downloads + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import kotlinx.android.synthetic.main.component_downloads.* +import kotlinx.android.synthetic.main.component_downloads.view.* +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.R +import org.mozilla.fenix.library.LibraryPageView +import org.mozilla.fenix.library.SelectionInteractor + +/** + * Interface for the DownloadViewInteractor. This interface is implemented by objects that want + * to respond to user interaction on the DownloadView + */ +interface DownloadViewInteractor : SelectionInteractor { + + /** + * Called on backpressed to exit edit mode + */ + fun onBackPressed(): Boolean +} + +/** + * View that contains and configures the Downloads List + */ +class DownloadView( + container: ViewGroup, + val interactor: DownloadInteractor +) : LibraryPageView(container), UserInteractionHandler { + + val view: View = LayoutInflater.from(container.context) + .inflate(R.layout.component_downloads, container, true) + + var mode: DownloadFragmentState.Mode = DownloadFragmentState.Mode.Normal + private set + + val downloadAdapter = DownloadAdapter(interactor) + private val layoutManager = LinearLayoutManager(container.context) + + init { + view.download_list.apply { + layoutManager = this@DownloadView.layoutManager + adapter = downloadAdapter + (itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + } + + fun update(state: DownloadFragmentState) { + + view.swipe_refresh.isEnabled = false + mode = state.mode + + updateEmptyState(state.items.isNotEmpty()) + + downloadAdapter.updateMode(state.mode) + downloadAdapter.updateDownloads(state.items) + + setUiForNormalMode( + context.getString(R.string.library_downloads) + ) + } + + fun updateEmptyState(userHasDownloads: Boolean) { + download_list.isVisible = userHasDownloads + download_empty_view.isVisible = !userHasDownloads + if (!userHasDownloads) { + download_empty_view.announceForAccessibility(context.getString(R.string.download_empty_message)) + } + } + + override fun onBackPressed(): Boolean { + return interactor.onBackPressed() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/viewholders/DownloadsListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/viewholders/DownloadsListItemViewHolder.kt new file mode 100644 index 000000000..c367ca674 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/viewholders/DownloadsListItemViewHolder.kt @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.library.downloads.viewholders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.download_list_item.view.* +import kotlinx.android.synthetic.main.library_site_item.view.* +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.hideAndDisable +import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.library.downloads.DownloadInteractor +import org.mozilla.fenix.library.downloads.DownloadItem +import mozilla.components.feature.downloads.toMegabyteString +import org.mozilla.fenix.ext.getIcon + +class DownloadsListItemViewHolder( + view: View, + private val downloadInteractor: DownloadInteractor, + private val selectionHolder: SelectionHolder +) : RecyclerView.ViewHolder(view) { + + private var item: DownloadItem? = null + + fun bind( + item: DownloadItem + ) { + itemView.download_layout.visibility = View.VISIBLE + itemView.download_layout.titleView.text = item.fileName + itemView.download_layout.urlView.text = item.size.toLong().toMegabyteString() + + itemView.download_layout.setSelectionInteractor(item, selectionHolder, downloadInteractor) + itemView.download_layout.changeSelected(item in selectionHolder.selectedItems) + + itemView.overflow_menu.hideAndDisable() + itemView.favicon.setImageResource(item.getIcon()) + itemView.favicon.isClickable = false + + this.item = item + } + + companion object { + const val LAYOUT_ID = R.layout.download_list_item + } +} diff --git a/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt b/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt index 16503f283..92be23ec6 100644 --- a/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/migration/MigrationProgressActivity.kt @@ -59,7 +59,7 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() { override fun onMigrationCompleted(results: MigrationResults) { // Enable clicking the finish button - migration_button_text_view.apply { + migration_button.apply { setOnClickListener { AbstractMigrationService.dismissNotification(context) @@ -78,6 +78,8 @@ class MigrationProgressActivity : AbstractMigrationProgressActivity() { startActivity(Intent(this@MigrationProgressActivity, HomeActivity::class.java)) } } + } + migration_button_text_view.apply { text = getString(R.string.migration_update_app_button, getString(R.string.app_name)) setTextColor(ContextCompat.getColor(context, R.color.white_color)) } diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt new file mode 100644 index 000000000..3e1945103 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.onboarding + +import android.content.Context +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.settings.SupportUtils + +class OnboardingController( + private val context: Context +) { + fun handleLearnMoreClicked() { + (context as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = SupportUtils.getFirefoxAccountSumoUrl(), + newTab = true, + from = BrowserDirection.FromHome + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingInteractor.kt b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingInteractor.kt new file mode 100644 index 000000000..ad11d459c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingInteractor.kt @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.onboarding + +class OnboardingInteractor(private val onboardingController: OnboardingController) { + + /** + * Called when the user clicks the learn more link + * @param url the url the suggestion was providing + */ + fun onLearnMoreClicked() = onboardingController.handleLearnMoreClicked() +} 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 4ed8d330c..6fdb7d0af 100644 --- a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt +++ b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingRadioButton.kt @@ -71,14 +71,14 @@ class OnboardingRadioButton( val spannableTitle = SpannableString(resources.getString(title)) spannableTitle.setTextSize(context, TITLE_TEXT_SIZE) - spannableTitle.setTextColor(context, R.color.primary_state_list_text_color) + spannableTitle.setTextColor(context, R.attr.primaryText) builder.append(spannableTitle) if (description != 0) { val spannableDescription = SpannableString(resources.getString(description)) spannableDescription.setTextSize(context, DESCRIPTION_TEXT_SIZE) - spannableDescription.setTextColor(context, R.color.secondary_state_list_text_color) + spannableDescription.setTextColor(context, R.attr.secondaryText) builder.append("\n") builder.append(spannableDescription) } diff --git a/app/src/main/java/org/mozilla/fenix/perf/VisualCompletenessQueue.kt b/app/src/main/java/org/mozilla/fenix/perf/VisualCompletenessQueue.kt new file mode 100644 index 000000000..2dd88c289 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/VisualCompletenessQueue.kt @@ -0,0 +1,33 @@ +/* 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.perf + +import android.view.View +import androidx.core.view.doOnPreDraw +import mozilla.components.support.utils.RunWhenReadyQueue +import java.lang.ref.WeakReference + +/** + * class for all functionality related to Visual completeness queue + */ +class VisualCompletenessQueue(val queue: RunWhenReadyQueue) { + @Suppress("MagicNumber") + val delay = 5000L + + /** + * + * @param containerWeakReference a weak reference to the root view of a view hierarchy. Weak + * reference is to avoid memory leak. + */ + fun attachViewToRunVisualCompletenessQueueLater(containerWeakReference: WeakReference) { + containerWeakReference.get()?.doOnPreDraw { + // This delay is temporary. We are delaying 5 seconds until the performance + // team can locate the real point of visual completeness. + it.postDelayed({ + queue.ready() + }, delay) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt index b2fe99221..8e668c7ea 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchController.kt @@ -61,6 +61,10 @@ class DefaultSearchController( // and open the crash list activity instead. activity.startActivity(Intent(activity, CrashListActivity::class.java)) } + "about:addons" -> { + val directions = SearchFragmentDirections.actionGlobalAddonsManagementFragment() + navController.navigateSafe(R.id.searchFragment, directions) + } "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(MANIFESTO)) else -> if (url.isNotBlank()) { openSearchOrUrl(url) diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt index d407a8d91..955dc0844 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchFragment.kt @@ -28,7 +28,7 @@ import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import kotlinx.android.synthetic.main.fragment_search.* import kotlinx.android.synthetic.main.fragment_search.view.* -import kotlinx.android.synthetic.main.search_suggestions_onboarding.view.* +import kotlinx.android.synthetic.main.search_suggestions_hint.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.concept.storage.HistoryStorage diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt index 3585efbbf..c4e81c041 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogController.kt @@ -44,6 +44,10 @@ class SearchDialogController( // and open the crash list activity instead. activity.startActivity(Intent(activity, CrashListActivity::class.java)) } + "about:addons" -> { + val directions = SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() + navController.navigateSafe(R.id.searchDialogFragment, directions) + } "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO)) else -> if (url.isNotBlank()) { openSearchOrUrl(url) @@ -159,6 +163,7 @@ class SearchDialogController( } override fun handleClickSearchEngineSettings() { + clearToolbarFocus() val directions = SearchDialogFragmentDirections.actionGlobalSearchEngineFragment() navController.navigateSafe(R.id.searchDialogFragment, directions) } diff --git a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt index 9c869aa81..7d0454ba8 100644 --- a/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/searchdialog/SearchDialogFragment.kt @@ -4,44 +4,80 @@ package org.mozilla.fenix.searchdialog +import android.app.Activity import android.app.Dialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.graphics.Typeface import android.os.Bundle +import android.speech.RecognizerIntent +import android.text.style.StyleSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewStub +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatDialogFragment import androidx.constraintlayout.widget.ConstraintProperties.BOTTOM import androidx.constraintlayout.widget.ConstraintProperties.PARENT_ID import androidx.constraintlayout.widget.ConstraintProperties.TOP import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import kotlinx.android.synthetic.main.fragment_search.view.* import kotlinx.android.synthetic.main.fragment_search_dialog.* +import kotlinx.android.synthetic.main.fragment_search_dialog.fill_link_from_clipboard +import kotlinx.android.synthetic.main.fragment_search_dialog.pill_wrapper +import kotlinx.android.synthetic.main.fragment_search_dialog.qr_scan_button +import kotlinx.android.synthetic.main.fragment_search_dialog.toolbar +import kotlinx.android.synthetic.main.fragment_search_dialog.view.* +import kotlinx.android.synthetic.main.search_suggestions_hint.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.feature.qr.QrFeature import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.ktx.android.content.hasCamera +import mozilla.components.support.ktx.android.content.res.getSpanned import mozilla.components.support.ktx.android.view.hideKeyboard +import mozilla.components.ui.autocomplete.InlineAutocompleteEditText +import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.searchengine.CustomSearchEngineStore +import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.toolbar.ToolbarPosition +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.search.SearchFragmentAction +import org.mozilla.fenix.search.SearchFragmentState import org.mozilla.fenix.search.SearchFragmentStore import org.mozilla.fenix.search.SearchInteractor import org.mozilla.fenix.search.awesomebar.AwesomeBarView import org.mozilla.fenix.search.createInitialSearchFragmentState import org.mozilla.fenix.search.toolbar.ToolbarView +import org.mozilla.fenix.settings.SupportUtils +import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener +import org.mozilla.fenix.widget.VoiceSearchActivity typealias SearchDialogFragmentStore = SearchFragmentStore typealias SearchDialogInteractor = SearchInteractor +@SuppressWarnings("LargeClass", "TooManyFunctions") class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { - private lateinit var interactor: SearchDialogInteractor private lateinit var store: SearchDialogFragmentStore private lateinit var toolbarView: ToolbarView private lateinit var awesomeBarView: AwesomeBarView + private var firstUpdate = true + + private val qrFeature = ViewBoundFeatureWrapper() + private val speechIntent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -64,6 +100,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { val args by navArgs() val view = inflater.inflate(R.layout.fragment_search_dialog, container, false) + requireComponents.analytics.metrics.track(Event.InteractWithSearchURLArea) + store = SearchDialogFragmentStore( createInitialSearchFragmentState( activity as HomeActivity, @@ -96,21 +134,196 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { false, view.toolbar, requireComponents.core.engine - ) + ).also(::addSearchButton) awesomeBarView = AwesomeBarView( requireContext(), interactor, - view.awesomeBar + view.awesome_bar ) + setShortcutsChangedListener(CustomSearchEngineStore.PREF_FILE_SEARCH_ENGINES) + setShortcutsChangedListener(FenixSearchEngineProvider.PREF_FILE_SEARCH_ENGINES) + + view.awesome_bar.setOnTouchListener { _, _ -> + view.hideKeyboard() + false + } + + awesomeBarView.view.setOnEditSuggestionListener(toolbarView.view::setSearchTerms) + + val urlView = toolbarView.view + .findViewById(R.id.mozac_browser_toolbar_edit_url_view) + urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + + val isPrivate = (requireActivity() as HomeActivity).browsingModeManager.mode.isPrivate + requireComponents.core.engine.speculativeCreateSession(isPrivate) + return view } @ExperimentalCoroutinesApi + @SuppressWarnings("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setupConstraints(view) + + search_wrapper.setOnClickListener { + it.hideKeyboard() + dismissAllowingStateLoss() + } + + view.search_engines_shortcut_button.setOnClickListener { + interactor.onSearchShortcutsButtonClicked() + } + + qr_scan_button.visibility = if (context?.hasCamera() == true) View.VISIBLE else View.GONE + + qr_scan_button.setOnClickListener { + if (!requireContext().hasCamera()) { return@setOnClickListener } + + toolbarView.view.clearFocus() + requireComponents.analytics.metrics.track(Event.QRScannerOpened) + qrFeature.get()?.scan(R.id.search_wrapper) + } + + fill_link_from_clipboard.setOnClickListener { + (activity as HomeActivity) + .openToBrowserAndLoad( + searchTermOrURL = requireContext().components.clipboardHandler.url ?: "", + newTab = store.state.tabId == null, + from = BrowserDirection.FromSearchDialog + ) + } + + qrFeature.set( + createQrFeature(), + owner = this, + view = view + ) + + val stubListener = ViewStub.OnInflateListener { _, inflated -> + inflated.learn_more.setOnClickListener { + (activity as HomeActivity) + .openToBrowserAndLoad( + searchTermOrURL = SupportUtils.getGenericSumoURLForTopic( + SupportUtils.SumoTopic.SEARCH_SUGGESTION + ), + newTab = store.state.tabId == null, + from = BrowserDirection.FromSearch + ) + } + + inflated.allow.setOnClickListener { + inflated.visibility = View.GONE + requireContext().settings().also { + it.shouldShowSearchSuggestionsInPrivate = true + it.showSearchSuggestionsInPrivateOnboardingFinished = true + } + store.dispatch(SearchFragmentAction.SetShowSearchSuggestions(true)) + store.dispatch(SearchFragmentAction.AllowSearchSuggestionsInPrivateModePrompt(false)) + requireComponents.analytics.metrics.track(Event.PrivateBrowsingShowSearchSuggestions) + } + + inflated.dismiss.setOnClickListener { + inflated.visibility = View.GONE + requireContext().settings().also { + it.shouldShowSearchSuggestionsInPrivate = false + it.showSearchSuggestionsInPrivateOnboardingFinished = true + } + } + + inflated.text.text = + getString(R.string.search_suggestions_onboarding_text, getString(R.string.app_name)) + + inflated.title.text = + getString(R.string.search_suggestions_onboarding_title) + } + + view.search_suggestions_hint.setOnInflateListener((stubListener)) + + consumeFrom(store) { + val shouldShowAwesomebar = + !firstUpdate && + it.query.isNotBlank() || + it.showSearchShortcuts + + awesome_bar?.visibility = if (shouldShowAwesomebar) View.VISIBLE else View.INVISIBLE + updateSearchSuggestionsHintVisibility(it) + updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url) + updateToolbarContentDescription(it) + toolbarView.update(it) + awesomeBarView.update(it) + firstUpdate = false + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { + if (requestCode == VoiceSearchActivity.SPEECH_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + intent?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)?.first()?.also { + toolbarView.view.edit.updateUrl(url = it, shouldHighlight = true) + interactor.onTextChanged(it) + toolbarView.view.edit.focus() + } + } + } + + override fun onBackPressed(): Boolean { + return when { + qrFeature.onBackPressed() -> { + toolbarView.view.edit.focus() + view?.qr_scan_button?.isChecked = false + toolbarView.view.requestFocus() + true + } + else -> { + view?.hideKeyboard() + dismissAllowingStateLoss() + true + } + } + } + + private fun createQrFeature(): QrFeature { + return QrFeature( + requireContext(), + fragmentManager = childFragmentManager, + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_CAMERA_PERMISSIONS) + }, + onScanResult = { result -> + qr_scan_button.isChecked = false + activity?.let { + AlertDialog.Builder(it).apply { + val spannable = resources.getSpanned( + R.string.qr_scanner_confirmation_dialog_message, + getString(R.string.app_name) to StyleSpan(Typeface.BOLD), + result to StyleSpan(Typeface.ITALIC) + ) + setMessage(spannable) + setNegativeButton(R.string.qr_scanner_dialog_negative) { dialog: DialogInterface, _ -> + requireComponents.analytics.metrics.track(Event.QRScannerNavigationDenied) + dialog.cancel() + } + setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ -> + requireComponents.analytics.metrics.track(Event.QRScannerNavigationAllowed) + (activity as HomeActivity) + .openToBrowserAndLoad( + searchTermOrURL = result, + newTab = store.state.tabId == null, + from = BrowserDirection.FromSearch + ) + dialog.dismiss() + } + create() + }.show() + requireComponents.analytics.metrics.track(Event.QRScannerPromptDisplayed) + } + }) + } + + private fun setupConstraints(view: View) { if (view.context.settings().toolbarPosition == ToolbarPosition.BOTTOM) { ConstraintSet().apply { clone(search_wrapper) @@ -118,31 +331,91 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { clear(toolbar.id, TOP) connect(toolbar.id, BOTTOM, PARENT_ID, BOTTOM) - clear(awesomeBar.id, TOP) - clear(awesomeBar.id, BOTTOM) - connect(awesomeBar.id, TOP, PARENT_ID, TOP) - connect(awesomeBar.id, BOTTOM, toolbar.id, TOP) + clear(awesome_bar.id, TOP) + clear(pill_wrapper.id, BOTTOM) + connect(awesome_bar.id, TOP, PARENT_ID, TOP) + connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP) + + clear(fill_link_from_clipboard.id, TOP) + connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP) applyTo(search_wrapper) } } + } - search_wrapper.setOnClickListener { - it.hideKeyboard() - dismissAllowingStateLoss() + private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) { + view?.apply { + val showHint = state.showSearchSuggestionsHint && !state.showSearchShortcuts + findViewById(R.id.search_suggestions_hint)?.isVisible = showHint + search_suggestions_hint_divider?.isVisible = showHint } + } - consumeFrom(store) { - awesomeBar?.visibility = if (it.query.isEmpty()) View.INVISIBLE else View.VISIBLE - toolbarView.update(it) - awesomeBarView.update(it) + private fun addSearchButton(toolbarView: ToolbarView) { + toolbarView.view.addEditAction( + BrowserToolbar.Button( + ContextCompat.getDrawable(requireContext(), R.drawable.ic_microphone)!!, + requireContext().getString(R.string.voice_search_content_description), + visible = { + store.state.searchEngineSource.searchEngine.identifier.contains("google") && + isSpeechAvailable() && + requireContext().settings().shouldShowVoiceSearch + }, + listener = ::launchVoiceSearch + ) + ) + } + + private fun launchVoiceSearch() { + // Note if a user disables speech while the app is on the search fragment + // the voice button will still be available and *will* cause a crash if tapped, + // since the `visible` call is only checked on create. In order to avoid extra complexity + // around such a small edge case, we make the button have no functionality in this case. + if (!isSpeechAvailable()) { return } + + requireComponents.analytics.metrics.track(Event.VoiceSearchTapped) + speechIntent.apply { + putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM) + putExtra(RecognizerIntent.EXTRA_PROMPT, requireContext().getString(R.string.voice_search_explainer)) } + startActivityForResult(speechIntent, VoiceSearchActivity.SPEECH_REQUEST_CODE) } - override fun onBackPressed(): Boolean { - view?.hideKeyboard() - dismissAllowingStateLoss() + private fun isSpeechAvailable(): Boolean = speechIntent.resolveActivity(requireContext().packageManager) != null + + private fun setShortcutsChangedListener(preferenceFileName: String) { + requireContext().getSharedPreferences( + preferenceFileName, + Context.MODE_PRIVATE + ).registerOnSharedPreferenceChangeListener(viewLifecycleOwner) { _, _ -> + awesomeBarView.update(store.state) + } + } + + private fun updateClipboardSuggestion(searchState: SearchFragmentState, clipboardUrl: String?) { + val shouldShowView = searchState.showClipboardSuggestions && + searchState.query.isEmpty() && + !clipboardUrl.isNullOrEmpty() && + !searchState.showSearchShortcuts + + fill_link_from_clipboard.visibility = if (shouldShowView) View.VISIBLE else View.GONE + clipboard_url.text = clipboardUrl + + if (clipboardUrl != null && !((activity as HomeActivity).browsingModeManager.mode.isPrivate)) { + requireComponents.core.engine.speculativeConnect(clipboardUrl) + } + } + + private fun updateToolbarContentDescription(searchState: SearchFragmentState) { + val urlView = toolbarView.view + .findViewById(R.id.mozac_browser_toolbar_edit_url_view) + toolbarView.view.contentDescription = + searchState.searchEngineSource.searchEngine.name + ", " + urlView.hint + urlView?.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO + } - return true + companion object { + private const val REQUEST_CODE_CAMERA_PERMISSIONS = 1 } } diff --git a/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt index 2684b0388..6f46fb997 100644 --- a/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt +++ b/app/src/main/java/org/mozilla/fenix/session/PerformanceActivityLifecycleCallbacks.kt @@ -51,7 +51,7 @@ class PerformanceActivityLifecycleCallbacks( if (activity is HomeActivity) { // We should delay the visualCompletenessQueue when reaching the HomeActivity // to ensure all tasks are delayed until after visual completeness - activity.postVisualCompletenessQueue(visualCompletenessQueue) + activity.setVisualCompletenessQueueReady() } else if (shouldStartVisualCompletenessQueueImmediately()) { // If we do not go through the home activity, we have to start the tasks // immediately to avoid spending time implementing it. diff --git a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt index 4686a04a7..bdf658647 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/CustomizationFragment.kt @@ -9,8 +9,10 @@ import android.os.Build import android.os.Build.VERSION.SDK_INT import android.os.Bundle import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.toolbar.ToolbarPosition @@ -54,6 +56,7 @@ class CustomizationFragment : PreferenceFragmentCompat() { bindAutoBatteryTheme() setupRadioGroups() setupToolbarCategory() + setupHomeCategory() } private fun setupRadioGroups() { @@ -136,4 +139,15 @@ class CustomizationFragment : PreferenceFragmentCompat() { addToRadioGroup(topPreference, bottomPreference) } + + private fun setupHomeCategory() { + requirePreference(R.string.pref_home_category).apply { + isVisible = FeatureFlags.topFrecentSite + } + requirePreference(R.string.pref_key_enable_top_frecent_sites).apply { + isVisible = FeatureFlags.topFrecentSite + isChecked = context.settings().showTopFrecentSites + onPreferenceChangeListener = SharedPreferenceUpdater() + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt index c3e663104..0d15672bd 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt @@ -5,8 +5,14 @@ package org.mozilla.fenix.settings import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference +import com.google.android.play.core.ktx.launchReview +import com.google.android.play.core.ktx.requestReview +import com.google.android.play.core.review.ReviewManagerFactory +import kotlinx.coroutines.launch import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.ext.settings @@ -25,9 +31,9 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey) - requirePreference(R.string.pref_key_use_new_search_experience).apply { - isVisible = FeatureFlags.newSearchExperience - isChecked = context.settings().useNewSearchExperience + requirePreference(R.string.pref_key_enable_top_frecent_sites).apply { + isVisible = FeatureFlags.topFrecentSite + isChecked = context.settings().showTopFrecentSites onPreferenceChangeListener = SharedPreferenceUpdater() } @@ -36,5 +42,22 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { isChecked = context.settings().waitToShowPageUntilFirstPaint onPreferenceChangeListener = SharedPreferenceUpdater() } + + requirePreference(R.string.pref_key_synced_tabs_tabs_tray).apply { + isVisible = FeatureFlags.syncedTabsInTabsTray + isChecked = context.settings().syncedTabsInTabsTray + onPreferenceChangeListener = SharedPreferenceUpdater() + } + + requirePreference(R.string.pref_key_temp_review_prompt).apply { + setOnPreferenceClickListener { + viewLifecycleOwner.lifecycleScope.launch { + val manager = ReviewManagerFactory.create(requireContext()) + val reviewInfo = manager.requestReview() + manager.launchReview(requireActivity(), reviewInfo) + } + true + } + } } } 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 f74e27bb4..86c117bd2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -25,7 +25,6 @@ import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.Profile -import mozilla.components.support.ktx.android.content.hasCamera import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity @@ -36,6 +35,7 @@ import org.mozilla.fenix.ext.application import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.metrics +import org.mozilla.fenix.ext.navigateToNotificationsSettings import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -190,16 +190,7 @@ class SettingsFragment : PreferenceFragmentCompat() { val directions: NavDirections? = when (preference.key) { resources.getString(R.string.pref_key_sign_in) -> { - // App can be installed on devices with no camera modules. Like Android TV boxes. - // Let's skip presenting the option to sign in by scanning a qr code in this case - // and default to login with email and password. - if (requireContext().hasCamera()) { - SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment() - } else { - requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext()) - requireComponents.analytics.metrics.track(Event.SyncAuthUseEmail) - null - } + SettingsFragmentDirections.actionSettingsFragmentToTurnOnSyncFragment() } resources.getString(R.string.pref_key_search_settings) -> { SettingsFragmentDirections.actionSettingsFragmentToSearchEngineFragment() @@ -270,6 +261,10 @@ class SettingsFragment : PreferenceFragmentCompat() { resources.getString(R.string.pref_key_delete_browsing_data_on_quit_preference) -> { SettingsFragmentDirections.actionSettingsFragmentToDeleteBrowsingDataOnQuitFragment() } + resources.getString(R.string.pref_key_notifications) -> { + context?.navigateToNotificationsSettings() + null + } resources.getString(R.string.pref_key_customize) -> { SettingsFragmentDirections.actionSettingsFragmentToCustomizationFragment() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index a1a613cc1..3e1bae04a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -74,6 +74,10 @@ object SupportUtils { return "https://support.mozilla.org/$langTag/kb/$escapedTopic" } + fun getFirefoxAccountSumoUrl(): String { + return "https://support.mozilla.org/kb/access-mozilla-services-firefox-account" + } + fun getMozillaPageUrl(page: MozillaPage, locale: Locale = Locale.getDefault()): String { val path = page.path val langTag = getLanguageTag(locale) 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 3fd152943..8f01a1e3b 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 @@ -12,6 +12,7 @@ import android.view.View import android.view.ViewGroup import androidx.core.content.pm.PackageInfoCompat import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.DividerItemDecoration import kotlinx.android.synthetic.main.fragment_about.* import org.mozilla.fenix.BrowserDirection @@ -167,8 +168,8 @@ class AboutFragment : Fragment(), AboutPageListener { } private fun openLibrariesPage() { - val intent = Intent(requireContext(), AboutLibrariesActivity::class.java) - startActivity(intent) + val navController = findNavController() + navController.navigate(R.id.action_aboutFragment_to_aboutLibrariesFragment) } override fun onAboutItemClicked(item: AboutItem) { @@ -185,18 +186,12 @@ class AboutFragment : Fragment(), AboutPageListener { PRIVACY_NOTICE -> { requireComponents.analytics.metrics.track(Event.PrivacyNoticeTapped) } - RIGHTS -> { - requireComponents.analytics.metrics.track(Event.RightsTapped) - } - LICENSING_INFO -> { - requireComponents.analytics.metrics.track(Event.LicensingTapped) - } + LICENSING_INFO, RIGHTS -> {} // no telemetry needed } openLinkInNormalTab(item.url) } is AboutItem.Libraries -> { - requireComponents.analytics.metrics.track(Event.LibrariesThatWeUseTapped) openLibrariesPage() } is AboutItem.Crashes -> { diff --git a/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt similarity index 80% rename from app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt rename to app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt index bf69fb2f3..bf2e2b6a8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/about/AboutLibrariesFragment.kt @@ -7,12 +7,15 @@ package org.mozilla.fenix.settings.about import android.graphics.Typeface import android.os.Bundle import android.text.util.Linkify +import android.view.View import android.widget.ArrayAdapter import android.widget.ListView import android.widget.TextView import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import kotlinx.android.synthetic.main.fragment_about_libraries.view.* import org.mozilla.fenix.R +import org.mozilla.fenix.ext.showToolbar import java.nio.charset.Charset import java.util.Locale @@ -27,33 +30,24 @@ import java.util.Locale * such as AboutLibraries (https://github.com/mikepenz/AboutLibraries) * but we considered the risk of introducing such third-party dependency * to Fenix too high. Therefore, we use Google's gradle plugin to - * extract the dependencies and their licenses, and this activity + * extract the dependencies and their licenses, and this fragment * to show the extracted licenses to the end-user. */ -class AboutLibrariesActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - +class AboutLibrariesFragment : Fragment(R.layout.fragment_about_libraries) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val appName = getString(R.string.app_name) - title = getString(R.string.open_source_licenses_title, appName) - setContentView(R.layout.about_libraries_activity) - - setSupportActionBar(findViewById(R.id.toolbar)) - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowHomeEnabled(true) - - setupLibrariesListView() - } + showToolbar(getString(R.string.open_source_licenses_title, appName)) - override fun onSupportNavigateUp(): Boolean { - onBackPressed() - return true + setupLibrariesListView(view.about_libraries_listview) } - private fun setupLibrariesListView() { + private fun setupLibrariesListView(listView: ListView) { val libraries = parseLibraries() - val listView = findViewById(R.id.about_libraries_listview) - listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, libraries) + listView.adapter = ArrayAdapter( + listView.context, + android.R.layout.simple_list_item_1, + libraries + ) listView.setOnItemClickListener { _, _, position, _ -> showLicenseDialog(libraries[position]) } @@ -95,7 +89,7 @@ class AboutLibrariesActivity : AppCompatActivity() { } private fun showLicenseDialog(libraryItem: LibraryItem) { - val dialog = AlertDialog.Builder(this) + val dialog = AlertDialog.Builder(requireContext()) .setTitle(libraryItem.name) .setMessage(libraryItem.license) .create() 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 0eab4909c..732f89671 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 @@ -36,7 +36,6 @@ 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.support.ktx.android.util.dpToPx -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider @@ -271,9 +270,8 @@ class AccountSettingsFragment : PreferenceFragmentCompat() { isChecked = syncEnginesStatus.getOrElse(SyncEngine.Passwords) { true } } requirePreference(R.string.pref_key_sync_tabs).apply { - isVisible = FeatureFlags.syncedTabs isEnabled = syncEnginesStatus.containsKey(SyncEngine.Tabs) - isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { FeatureFlags.syncedTabs } + isChecked = syncEnginesStatus.getOrElse(SyncEngine.Tabs) { true } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt index ceedd2309..acfa74c2a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/TurnOnSyncFragment.kt @@ -17,6 +17,7 @@ import kotlinx.android.synthetic.main.fragment_turn_on_sync.view.* import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount +import mozilla.components.support.ktx.android.content.hasCamera import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event @@ -26,15 +27,11 @@ import org.mozilla.fenix.ext.showToolbar class TurnOnSyncFragment : Fragment(), AccountObserver { private val args by navArgs() + private var shouldLoginJustWithEmail = false + private var pairWithEmailStarted = false private val signInClickListener = View.OnClickListener { - requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext()) - requireComponents.analytics.metrics.track(Event.SyncAuthUseEmail) - // TODO The sign-in web content populates session history, - // so pressing "back" after signing in won't take us back into the settings screen, but rather up the - // session history stack. - // We could auto-close this tab once we get to the end of the authentication process? - // Via an interceptor, perhaps. + navigateToPairWithEmail() } private val paringClickListener = View.OnClickListener { @@ -46,6 +43,14 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requireComponents.analytics.metrics.track(Event.SyncAuthOpened) + + // App can be installed on devices with no camera modules. Like Android TV boxes. + // Let's skip presenting the option to sign in by scanning a qr code in this case + // and default to login with email and password. + shouldLoginJustWithEmail = !requireContext().hasCamera() + if (shouldLoginJustWithEmail) { + navigateToPairWithEmail() + } } override fun onDestroy() { @@ -55,16 +60,28 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { override fun onResume() { super.onResume() - if (requireComponents.backgroundServices.accountManager.authenticatedAccount() != null) { + if (pairWithEmailStarted || + requireComponents.backgroundServices.accountManager.authenticatedAccount() != null) { + findNavController().popBackStack() return } - requireComponents.backgroundServices.accountManager.register(this, owner = this) - showToolbar(getString(R.string.preferences_sync)) + if (shouldLoginJustWithEmail) { + // Next time onResume is called, after returning from pairing with email this Fragment will be popped. + pairWithEmailStarted = true + } else { + requireComponents.backgroundServices.accountManager.register(this, owner = this) + showToolbar(getString(R.string.preferences_sync)) + } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (shouldLoginJustWithEmail) { + // Headless fragment. Don't need UI if we're taking the user to another screen. + return null + } + val view = inflater.inflate(R.layout.fragment_turn_on_sync, container, false) view.signInScanButton.setOnClickListener(paringClickListener) view.signInEmailButton.setOnClickListener(signInClickListener) @@ -99,4 +116,14 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { .show() } } + + private fun navigateToPairWithEmail() { + requireComponents.services.accountsAuthFeature.beginAuthentication(requireContext()) + requireComponents.analytics.metrics.track(Event.SyncAuthUseEmail) + // TODO The sign-in web content populates session history, + // so pressing "back" after signing in won't take us back into the settings screen, but rather up the + // session history stack. + // We could auto-close this tab once we get to the end of the authentication process? + // Via an interceptor, perhaps. + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt index 81d742d39..49e313b57 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/SyncLoginsPreferenceView.kt @@ -4,21 +4,16 @@ package org.mozilla.fenix.settings.logins -import android.content.Context import androidx.lifecycle.LifecycleOwner import androidx.navigation.NavController import androidx.preference.Preference import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.OAuthAccount -import mozilla.components.feature.accounts.FirefoxAccountsAuthFeature import mozilla.components.service.fxa.SyncEngine import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.SyncEnginesStorage -import mozilla.components.support.ktx.android.content.hasCamera import org.mozilla.fenix.R -import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.settings.logins.fragment.SavedLoginsAuthFragmentDirections /** @@ -28,9 +23,7 @@ class SyncLoginsPreferenceView( private val syncLoginsPreference: Preference, lifecycleOwner: LifecycleOwner, accountManager: FxaAccountManager, - private val navController: NavController, - private val accountsAuthFeature: FirefoxAccountsAuthFeature, - private val metrics: MetricController + private val navController: NavController ) { init { @@ -75,15 +68,7 @@ class SyncLoginsPreferenceView( syncLoginsPreference.apply { summary = context.getString(R.string.preferences_passwords_sync_logins_sign_in) setOnPreferenceClickListener { - // App can be installed on devices with no camera modules. Like Android TV boxes. - // Let's skip presenting the option to sign in by scanning a qr code in this case - // and default to login with email and password. - if (context.hasCamera()) { - navigateToTurnOnSyncFragment() - } else { - navigateToPairWithEmail(context) - } - + navigateToTurnOnSyncFragment() true } } @@ -117,9 +102,4 @@ class SyncLoginsPreferenceView( val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment() navController.navigate(directions) } - - private fun navigateToPairWithEmail(context: Context) { - accountsAuthFeature.beginAuthentication(context) - metrics.track(Event.SyncAuthUseEmail) - } } 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 acd6f1320..ed452e33b 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 @@ -25,7 +25,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ObsoleteCoroutinesApi import mozilla.components.lib.state.ext.consumeFrom import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar @@ -169,11 +168,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) { } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - if (FeatureFlags.loginsEdit) { - inflater.inflate(R.menu.login_options_menu, menu) - } else { - inflater.inflate(R.menu.login_delete, menu) - } + inflater.inflate(R.menu.login_options_menu, menu) this.menu = menu } 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 a7493f99b..4246beccd 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 @@ -145,9 +145,7 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { requirePreference(R.string.pref_key_password_sync_logins), lifecycleOwner = viewLifecycleOwner, accountManager = requireComponents.backgroundServices.accountManager, - navController = findNavController(), - accountsAuthFeature = requireComponents.services.accountsAuthFeature, - metrics = requireComponents.analytics.metrics + navController = findNavController() ) togglePrefsEnabledWhileAuthenticating(enabled = true) diff --git a/app/src/main/java/org/mozilla/fenix/shortcut/CreateShortcutFragment.kt b/app/src/main/java/org/mozilla/fenix/shortcut/CreateShortcutFragment.kt index 9918a677e..836bfce84 100644 --- a/app/src/main/java/org/mozilla/fenix/shortcut/CreateShortcutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/shortcut/CreateShortcutFragment.kt @@ -41,7 +41,7 @@ class CreateShortcutFragment : DialogFragment() { cancel_button.setOnClickListener { dismiss() } add_button.setOnClickListener { - val text = shortcut_text.text.toString() + val text = shortcut_text.text.toString().trim() requireActivity().lifecycleScope.launch { requireComponents.useCases.webAppUseCases.addToHomescreen(text) } @@ -57,8 +57,9 @@ class CreateShortcutFragment : DialogFragment() { } private fun updateAddButtonEnabledState() { - add_button.isEnabled = shortcut_text.text.isNotEmpty() - add_button.alpha = if (shortcut_text.text.isNotEmpty()) ENABLED_ALPHA else DISABLED_ALPHA + val text = shortcut_text.text + add_button.isEnabled = text.isNotBlank() + add_button.alpha = if (text.isNotBlank()) ENABLED_ALPHA else DISABLED_ALPHA } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt index e51419df4..6e30a4711 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsAdapter.kt @@ -10,14 +10,18 @@ import androidx.navigation.NavController import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.sync.SyncedTabsViewHolder.DeviceViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.ErrorViewHolder +import org.mozilla.fenix.sync.SyncedTabsViewHolder.NoTabsViewHolder import org.mozilla.fenix.sync.SyncedTabsViewHolder.TabViewHolder +import org.mozilla.fenix.sync.SyncedTabsViewHolder.TitleViewHolder +import org.mozilla.fenix.sync.ext.toAdapterList import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.concept.sync.Device as SyncDevice class SyncedTabsAdapter( - private val listener: (SyncTab) -> Unit + private val newListener: SyncedTabsView.Listener ) : ListAdapter(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SyncedTabsViewHolder { @@ -27,30 +31,26 @@ class SyncedTabsAdapter( DeviceViewHolder.LAYOUT_ID -> DeviceViewHolder(itemView) TabViewHolder.LAYOUT_ID -> TabViewHolder(itemView) ErrorViewHolder.LAYOUT_ID -> ErrorViewHolder(itemView) + TitleViewHolder.LAYOUT_ID -> TitleViewHolder(itemView) + NoTabsViewHolder.LAYOUT_ID -> NoTabsViewHolder(itemView) else -> throw IllegalStateException() } } override fun onBindViewHolder(holder: SyncedTabsViewHolder, position: Int) { - holder.bind(getItem(position), listener) + holder.bind(getItem(position), newListener) } override fun getItemViewType(position: Int) = when (getItem(position)) { is AdapterItem.Device -> DeviceViewHolder.LAYOUT_ID is AdapterItem.Tab -> TabViewHolder.LAYOUT_ID is AdapterItem.Error -> ErrorViewHolder.LAYOUT_ID + is AdapterItem.Title -> TitleViewHolder.LAYOUT_ID + is AdapterItem.NoTabs -> NoTabsViewHolder.LAYOUT_ID } fun updateData(syncedTabs: List) { - val allDeviceTabs = mutableListOf() - - syncedTabs.forEach { (device, tabs) -> - if (tabs.isNotEmpty()) { - allDeviceTabs.add(AdapterItem.Device(device)) - tabs.mapTo(allDeviceTabs) { AdapterItem.Tab(it) } - } - } - + val allDeviceTabs = syncedTabs.toAdapterList() submitList(allDeviceTabs) } @@ -59,7 +59,11 @@ class SyncedTabsAdapter( when (oldItem) { is AdapterItem.Device -> newItem is AdapterItem.Device && oldItem.device.id == newItem.device.id - is AdapterItem.Tab, is AdapterItem.Error -> + is AdapterItem.NoTabs -> + newItem is AdapterItem.NoTabs && oldItem.device.id == newItem.device.id + is AdapterItem.Tab, + is AdapterItem.Error, + is AdapterItem.Title -> oldItem == newItem } @@ -68,9 +72,35 @@ class SyncedTabsAdapter( oldItem == newItem } + /** + * The various types of adapter items that can be found in a [SyncedTabsAdapter]. + */ sealed class AdapterItem { + + /** + * A title header of the Synced Tabs UI that has a refresh button in it. This may be seen + * only in some views depending on where the Synced Tabs UI is displayed. + */ + object Title : AdapterItem() + + /** + * A device header for displaying a synced device. + */ data class Device(val device: SyncDevice) : AdapterItem() + + /** + * A tab that was synced. + */ data class Tab(val tab: SyncTab) : AdapterItem() + + /** + * A placeholder for a device that has no tabs synced. + */ + data class NoTabs(val device: SyncDevice) : AdapterItem() + + /** + * A message displayed if an error was encountered. + */ data class Error( val descriptionResId: Int, val navController: NavController? = null diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt index 4059d5159..cd67fe06b 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt @@ -13,6 +13,7 @@ import mozilla.components.browser.storage.sync.Tab import mozilla.components.feature.syncedtabs.SyncedTabsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -22,6 +23,11 @@ import org.mozilla.fenix.library.LibraryPageFragment class SyncedTabsFragment : LibraryPageFragment() { private val syncedTabsFeature = ViewBoundFeatureWrapper() + init { + // Sanity-check: Remove this class when the feature flag is always enabled. + FeatureFlags.syncedTabsInTabsTray + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index 8308692ba..2d3b5c0a4 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.sync import android.content.Context import android.util.AttributeSet import android.widget.FrameLayout -import androidx.annotation.StringRes import androidx.fragment.app.findFragment import androidx.navigation.NavController import androidx.navigation.fragment.findNavController @@ -18,8 +17,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.browser.storage.sync.Tab import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R +import org.mozilla.fenix.sync.ext.toAdapterItem +import org.mozilla.fenix.sync.ext.toStringRes import java.lang.IllegalStateException class SyncedTabsLayout @JvmOverloads constructor( @@ -30,7 +33,7 @@ class SyncedTabsLayout @JvmOverloads constructor( override var listener: SyncedTabsView.Listener? = null - private val adapter = SyncedTabsAdapter { listener?.onTabClicked(it) } + private val adapter = SyncedTabsAdapter(ListenerDelegate { listener }) private val coroutineScope = CoroutineScope(Dispatchers.Main) init { @@ -40,6 +43,9 @@ class SyncedTabsLayout @JvmOverloads constructor( synced_tabs_list.adapter = adapter synced_tabs_pull_to_refresh.setOnRefreshListener { listener?.onRefresh() } + + // Sanity-check: Remove this class when the feature flag is always enabled. + FeatureFlags.syncedTabsInTabsTray } override fun onError(error: SyncedTabsView.ErrorType) { @@ -53,8 +59,8 @@ class SyncedTabsLayout @JvmOverloads constructor( null } - val descriptionResId = stringResourceForError(error) - val errorItem = getErrorItem(navController, error, descriptionResId) + val descriptionResId = error.toStringRes() + val errorItem = error.toAdapterItem(descriptionResId, navController) val errorList: List = listOf(errorItem) adapter.submitList(errorList) @@ -96,27 +102,21 @@ class SyncedTabsLayout @JvmOverloads constructor( SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> true } + } +} - internal fun stringResourceForError(error: SyncedTabsView.ErrorType) = when (error) { - SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device - SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message - SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth - SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs - } +/** + * We have to do this weird daisy-chaining of callbacks because the listener is nullable and + * when we get a null reference, we never get a new binding to the non-null listener. + */ +class ListenerDelegate( + private val listener: (() -> SyncedTabsView.Listener?) +) : SyncedTabsView.Listener { + override fun onRefresh() { + listener.invoke()?.onRefresh() + } - internal fun getErrorItem( - navController: NavController?, - error: SyncedTabsView.ErrorType, - @StringRes stringResId: Int - ): SyncedTabsAdapter.AdapterItem = when (error) { - SyncedTabsView.ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, - SyncedTabsView.ErrorType.SYNC_ENGINE_UNAVAILABLE, - SyncedTabsView.ErrorType.SYNC_NEEDS_REAUTHENTICATION, - SyncedTabsView.ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem - .Error(descriptionResId = stringResId) - SyncedTabsView.ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem - .Error(descriptionResId = stringResId, navController = navController) - } + override fun onTabClicked(tab: Tab) { + listener.invoke()?.onTabClicked(tab) } } diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt index 95abada88..4549f570e 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -7,29 +7,36 @@ package org.mozilla.fenix.sync import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.view.animation.Animation +import android.view.animation.AnimationUtils import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.sync_tabs_error_row.view.* import kotlinx.android.synthetic.main.sync_tabs_list_item.view.* import kotlinx.android.synthetic.main.view_synced_tabs_group.view.* -import mozilla.components.browser.storage.sync.Tab +import kotlinx.android.synthetic.main.view_synced_tabs_title.view.* import mozilla.components.concept.sync.DeviceType +import mozilla.components.feature.syncedtabs.view.SyncedTabsView import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem +/** + * The various view-holders that can be found in a [SyncedTabsAdapter]. For more + * descriptive information on the different types, see the docs for [AdapterItem]. + */ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - abstract fun bind(item: T, interactor: (Tab) -> Unit) + abstract fun bind(item: T, interactor: SyncedTabsView.Listener) class TabViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { - override fun bind(item: T, interactor: (Tab) -> Unit) { + override fun bind(item: T, interactor: SyncedTabsView.Listener) { bindTab(item as AdapterItem.Tab) itemView.setOnClickListener { - interactor(item.tab) + interactor.onTabClicked(item.tab) } } @@ -46,7 +53,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item class ErrorViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { - override fun bind(item: T, interactor: (Tab) -> Unit) { + override fun bind(item: T, interactor: SyncedTabsView.Listener) { val errorItem = item as AdapterItem.Error setErrorMargins() @@ -69,7 +76,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item class DeviceViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { - override fun bind(item: T, interactor: (Tab) -> Unit) { + override fun bind(item: T, interactor: SyncedTabsView.Listener) { bindHeader(item as AdapterItem.Device) } @@ -93,6 +100,36 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item } } + class NoTabsViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + override fun bind(item: T, interactor: SyncedTabsView.Listener) = Unit + + companion object { + const val LAYOUT_ID = R.layout.view_synced_tabs_no_item + } + } + + class TitleViewHolder(itemView: View) : SyncedTabsViewHolder(itemView) { + + override fun bind(item: T, interactor: SyncedTabsView.Listener) { + itemView.refresh_icon.setOnClickListener { v -> + val rotation = AnimationUtils.loadAnimation( + itemView.context, + R.anim.full_rotation + ).apply { + repeatCount = Animation.ABSOLUTE + } + + v.startAnimation(rotation) + + interactor.onRefresh() + } + } + + companion object { + const val LAYOUT_ID = R.layout.view_synced_tabs_title + } + } + internal fun setErrorMargins() { val lp = LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, diff --git a/app/src/main/java/org/mozilla/fenix/sync/ext/ErrorType.kt b/app/src/main/java/org/mozilla/fenix/sync/ext/ErrorType.kt new file mode 100644 index 000000000..1a24ba455 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/ext/ErrorType.kt @@ -0,0 +1,38 @@ +/* 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.sync.ext + +import androidx.annotation.StringRes +import androidx.navigation.NavController +import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType +import org.mozilla.fenix.R +import org.mozilla.fenix.sync.SyncedTabsAdapter + +/** + * Converts the error type to the appropriate matching string resource for displaying to the user. + */ +fun ErrorType.toStringRes() = when (this) { + ErrorType.MULTIPLE_DEVICES_UNAVAILABLE -> R.string.synced_tabs_connect_another_device + ErrorType.SYNC_ENGINE_UNAVAILABLE -> R.string.synced_tabs_enable_tab_syncing + ErrorType.SYNC_UNAVAILABLE -> R.string.synced_tabs_sign_in_message + ErrorType.SYNC_NEEDS_REAUTHENTICATION -> R.string.synced_tabs_reauth + ErrorType.NO_TABS_AVAILABLE -> R.string.synced_tabs_no_tabs +} + +/** + * Converts an error type to an [SyncedTabsAdapter.AdapterItem.Error]. + */ +fun ErrorType.toAdapterItem( + @StringRes stringResId: Int, + navController: NavController? = null +) = when (this) { + ErrorType.MULTIPLE_DEVICES_UNAVAILABLE, + ErrorType.SYNC_ENGINE_UNAVAILABLE, + ErrorType.SYNC_NEEDS_REAUTHENTICATION, + ErrorType.NO_TABS_AVAILABLE -> SyncedTabsAdapter.AdapterItem + .Error(descriptionResId = stringResId) + ErrorType.SYNC_UNAVAILABLE -> SyncedTabsAdapter.AdapterItem + .Error(descriptionResId = stringResId, navController = navController) +} diff --git a/app/src/main/java/org/mozilla/fenix/sync/ext/SyncedTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/sync/ext/SyncedTabsAdapter.kt new file mode 100644 index 000000000..18c2c1be9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/sync/ext/SyncedTabsAdapter.kt @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.sync.ext + +import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem + +/** + * Converts a list of [SyncedDeviceTabs] into a list of [AdapterItem]. + */ +fun List.toAdapterList() = asSequence().flatMap { (device, tabs) -> + + val deviceTabs = if (tabs.isEmpty()) { + sequenceOf(AdapterItem.NoTabs(device)) + } else { + tabs.asSequence().map { AdapterItem.Tab(it) } + } + + sequenceOf(AdapterItem.Device(device)) + deviceTabs +}.toList() diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt index 67eeba43b..f80e48406 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.widget.CheckedTextView import androidx.annotation.VisibleForTesting import androidx.core.content.ContextCompat +import androidx.core.view.updatePaddingRelative import androidx.recyclerview.widget.RecyclerView import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.R @@ -36,7 +37,7 @@ internal class CollectionsAdapter( override fun onBindViewHolder(holder: CollectionItemViewHolder, position: Int) { if (position == 0) { val displayMetrics = holder.textView.context.resources.displayMetrics - holder.textView.setPadding(NEW_COLLECTION_PADDING_START.dpToPx(displayMetrics), 0, 0, 0) + holder.textView.updatePaddingRelative(start = NEW_COLLECTION_PADDING_START.dpToPx(displayMetrics)) holder.textView.compoundDrawablePadding = NEW_COLLECTION_DRAWABLE_PADDING.dpToPx(displayMetrics) holder.textView.setCompoundDrawablesWithIntrinsicBounds( diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt new file mode 100644 index 000000000..8fbb87c63 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt @@ -0,0 +1,85 @@ +/* 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.tabtray + +import android.view.View +import androidx.fragment.app.FragmentManager.findFragment +import androidx.lifecycle.LifecycleOwner +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.ConcatAdapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.sync.ListenerDelegate +import org.mozilla.fenix.sync.SyncedTabsAdapter +import org.mozilla.fenix.sync.ext.toAdapterList +import org.mozilla.fenix.sync.ext.toAdapterItem +import org.mozilla.fenix.sync.ext.toStringRes +import kotlin.coroutines.CoroutineContext + +@OptIn(ExperimentalCoroutinesApi::class) +class SyncedTabsController( + lifecycleOwner: LifecycleOwner, + private val view: View, + store: TabTrayDialogFragmentStore, + private val concatAdapter: ConcatAdapter, + coroutineContext: CoroutineContext = Dispatchers.Main +) : SyncedTabsView { + override var listener: SyncedTabsView.Listener? = null + + val adapter = SyncedTabsAdapter(ListenerDelegate { listener }) + + private val scope: CoroutineScope = CoroutineScope(coroutineContext) + + init { + store.flowScoped(lifecycleOwner) { flow -> + flow.map { it.mode } + .ifChanged() + .drop(1) + .collect { mode -> + when (mode) { + is TabTrayDialogFragmentState.Mode.Normal -> { + concatAdapter.addAdapter(0, adapter) + } + is TabTrayDialogFragmentState.Mode.MultiSelect -> { + concatAdapter.removeAdapter(adapter) + } + } + } + } + } + + override fun displaySyncedTabs(syncedTabs: List) { + scope.launch { + val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList() + // Reverse layout for TabTrayView which does things backwards. + adapter.submitList(tabsList.reversed()) + } + } + + override fun onError(error: SyncedTabsView.ErrorType) { + scope.launch { + val navController: NavController? = try { + findFragment(view).findNavController() + } catch (exception: IllegalStateException) { + null + } + + val descriptionResId = error.toStringRes() + val errorItem = error.toAdapterItem(descriptionResId, navController) + + adapter.submitList(listOf(errorItem)) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index ad71ced1c..7a3a03eee 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -9,10 +9,12 @@ import androidx.navigation.NavController import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.storage.sync.Tab as SyncTab import mozilla.components.concept.engine.profiler.Profiler import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tabs.TabsUseCases +import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager @@ -30,6 +32,7 @@ interface TabTrayController { fun onNewTabTapped(private: Boolean) fun onTabTrayDismissed() fun onShareTabsClicked(private: Boolean) + fun onSyncedTabClicked(syncTab: SyncTab) fun onSaveToCollectionClicked(selectedTabs: Set) fun onCloseAllTabsClicked(private: Boolean) fun handleBackPressed(): Boolean @@ -59,6 +62,7 @@ interface TabTrayController { */ @Suppress("TooManyFunctions") class DefaultTabTrayController( + private val activity: HomeActivity, private val profiler: Profiler?, private val sessionManager: SessionManager, private val browsingModeManager: BrowsingModeManager, @@ -117,6 +121,14 @@ class DefaultTabTrayController( navController.navigate(directions) } + override fun onSyncedTabClicked(syncTab: SyncTab) { + activity.openToBrowserAndLoad( + searchTermOrURL = syncTab.active().url, + newTab = true, + from = BrowserDirection.FromTabTray + ) + } + @OptIn(ExperimentalCoroutinesApi::class) override fun onCloseAllTabsClicked(private: Boolean) { val sessionsToClose = if (private) { diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index cfe8b0f1c..3b64217f4 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -177,6 +177,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler adapter, interactor = TabTrayFragmentInteractor( DefaultTabTrayController( + activity = activity, profiler = activity.components.core.engine.profiler, sessionManager = activity.components.core.sessionManager, browsingModeManager = activity.browsingModeManager, @@ -191,10 +192,11 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler showAddNewCollectionDialog = ::showAddNewCollectionDialog ) ), + store = tabTrayDialogStore, isPrivate = isPrivate, startingInLandscape = requireContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE, - lifecycleScope = viewLifecycleOwner.lifecycleScope + lifecycleOwner = viewLifecycleOwner ) { private -> val filter: (TabSessionState) -> Boolean = { state -> private == state.content.private } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index 33bef403a..b6a65dd77 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabtray import mozilla.components.concept.tabstray.Tab +import mozilla.components.browser.storage.sync.Tab as SyncTab @Suppress("TooManyFunctions") interface TabTrayInteractor { @@ -33,6 +34,11 @@ interface TabTrayInteractor { */ fun onCloseAllTabsClicked(private: Boolean) + /** + * Called when the user clicks on a synced tab entry. + */ + fun onSyncedTabClicked(syncTab: SyncTab) + /** * Called when the physical back button is clicked. */ @@ -89,6 +95,10 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab controller.onCloseAllTabsClicked(private) } + override fun onSyncedTabClicked(syncTab: SyncTab) { + controller.onSyncedTabClicked(syncTab) + } + override fun onBackPressed(): Boolean { return controller.handleBackPressed() } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index 9b78836ff..4963c044e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -16,7 +16,9 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams -import androidx.lifecycle.LifecycleCoroutineScope +import androidx.core.view.updatePadding +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -36,6 +38,8 @@ 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.tabstray.TabViewHolder +import mozilla.components.feature.syncedtabs.SyncedTabsFeature +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.util.dpToPx import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event @@ -47,6 +51,7 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.tabtray.SaveToCollectionsButtonAdapter.MultiselectModeChange import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode import java.text.NumberFormat +import mozilla.components.browser.storage.sync.Tab as SyncTab /** * View that contains and configures the BrowserAwesomeBar @@ -56,11 +61,13 @@ class TabTrayView( private val container: ViewGroup, private val tabsAdapter: FenixTabsAdapter, private val interactor: TabTrayInteractor, + store: TabTrayDialogFragmentStore, isPrivate: Boolean, startingInLandscape: Boolean, - lifecycleScope: LifecycleCoroutineScope, + lifecycleOwner: LifecycleOwner, private val filterTabs: (Boolean) -> Unit ) : LayoutContainer, TabLayout.OnTabSelectedListener { + val lifecycleScope = lifecycleOwner.lifecycleScope val fabView = LayoutInflater.from(container.context) .inflate(R.layout.component_tabstray_fab, container, true) @@ -73,19 +80,25 @@ class TabTrayView( private val behavior = BottomSheetBehavior.from(view.tab_wrapper) + private val concatAdapter = ConcatAdapter(tabsAdapter) private val tabTrayItemMenu: TabTrayItemMenu private var menu: BrowserMenu? = null private var tabsTouchHelper: TabsTouchHelper private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) + private val syncedTabsController = SyncedTabsController(lifecycleOwner, view, store, concatAdapter) + private val syncedTabsFeature = ViewBoundFeatureWrapper() + private var hasLoaded = false override val containerView: View? get() = container + private val components = container.context.components + init { - container.context.components.analytics.metrics.track(Event.TabsTrayOpened) + components.analytics.metrics.track(Event.TabsTrayOpened) toggleFabText(isPrivate) @@ -102,7 +115,7 @@ class TabTrayView( override fun onStateChanged(bottomSheet: View, newState: Int) { if (newState == BottomSheetBehavior.STATE_HIDDEN) { - container.context.components.analytics.metrics.track(Event.TabsTrayClosed) + components.analytics.metrics.track(Event.TabsTrayClosed) interactor.onTabTrayDismissed() } } @@ -135,7 +148,20 @@ class TabTrayView( setTopOffset(startingInLandscape) - val concatAdapter = ConcatAdapter(tabsAdapter) + if (view.context.settings().syncedTabsInTabsTray) { + syncedTabsFeature.set( + feature = SyncedTabsFeature( + context = container.context, + storage = components.backgroundServices.syncedTabsStorage, + accountManager = components.backgroundServices.accountManager, + view = syncedTabsController, + lifecycleOwner = lifecycleOwner, + onTabClicked = ::handleTabClicked + ), + owner = lifecycleOwner, + view = view + ) + } view.tabsTray.apply { layoutManager = LinearLayoutManager(container.context).apply { @@ -156,6 +182,9 @@ class TabTrayView( // Put the 'Add to collections' button after the tabs have loaded. concatAdapter.addAdapter(0, collectionsButtonAdapter) + // Put the Synced Tabs adapter at the end. + concatAdapter.addAdapter(0, syncedTabsController.adapter) + if (hasAccessibilityEnabled) { tabsAdapter.notifyDataSetChanged() } @@ -193,7 +222,7 @@ class TabTrayView( } view.tab_tray_overflow.setOnClickListener { - container.context.components.analytics.metrics.track(Event.TabsTrayMenuOpened) + components.analytics.metrics.track(Event.TabsTrayMenuOpened) menu = tabTrayItemMenu.menuBuilder.build(container.context) menu?.show(it) ?.also { pu -> @@ -209,6 +238,10 @@ class TabTrayView( adjustNewTabButtonsForNormalMode() } + private fun handleTabClicked(tab: SyncTab) { + interactor.onSyncedTabClicked(tab) + } + private fun adjustNewTabButtonsForNormalMode() { view.tab_tray_new_tab.apply { isVisible = hasAccessibilityEnabled @@ -234,7 +267,7 @@ class TabTrayView( Event.NewTabTapped } - container.context.components.analytics.metrics.track(eventToSend) + components.analytics.metrics.track(eventToSend) } fun expand() { @@ -261,17 +294,14 @@ class TabTrayView( scrollToTab(view.context.components.core.store.state.selectedTabId) if (isPrivateModeSelected) { - container.context.components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped) + components.analytics.metrics.track(Event.TabsTrayPrivateModeTapped) } else { - container.context.components.analytics.metrics.track(Event.TabsTrayNormalModeTapped) + components.analytics.metrics.track(Event.TabsTrayNormalModeTapped) } } - override fun onTabReselected(tab: TabLayout.Tab?) { /*noop*/ - } - - override fun onTabUnselected(tab: TabLayout.Tab?) { /*noop*/ - } + override fun onTabReselected(tab: TabLayout.Tab?) = Unit + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit var mode: Mode = Mode.Normal private set @@ -464,7 +494,7 @@ class TabTrayView( private fun updateTabCounter(count: Int): String { if (count > MAX_VISIBLE_TABS) { - counter_text.setPadding(0, 0, 0, INFINITE_CHAR_PADDING_BOTTOM) + counter_text.updatePadding(bottom = INFINITE_CHAR_PADDING_BOTTOM) return SO_MANY_TABS_OPEN } return NumberFormat.getInstance().format(count.toLong()) @@ -513,7 +543,9 @@ class TabTrayView( // We offset the tab index by the number of items in the other adapters. // We add the offset, because the layoutManager is initialized with `reverseLayout`. - val recyclerViewIndex = selectedBrowserTabIndex + collectionsButtonAdapter.itemCount + val recyclerViewIndex = selectedBrowserTabIndex + + collectionsButtonAdapter.itemCount + + syncedTabsController.adapter.itemCount layoutManager?.scrollToPosition(recyclerViewIndex) } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt index 8ed15bf03..285e4d454 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt @@ -96,6 +96,8 @@ class TrackingProtectionOverlay( trackingOnboardingDialog.apply { setContentView(layout) setCancelable(false) + // removing title or setting it as an empty string does not prevent a11y services from assigning one + setTitle(" ") } trackingOnboardingDialog.window?.let { 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 1297eb5bb..e8a4cb7c1 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -54,6 +54,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { const val showLoginsSecureWarningMaxCount = 1 const val trackingProtectionOnboardingMaximumCount = 1 const val pwaVisitsToShowPromptMaxCount = 3 + const val topSitesMaxCount = 16 const val FENIX_PREFERENCES = "fenix_preferences" private const val showSearchWidgetCFRMaxCount = 3 @@ -62,6 +63,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { private const val ALLOWED_INT = 2 private const val CFR_COUNT_CONDITION_FOCUS_INSTALLED = 1 private const val CFR_COUNT_CONDITION_FOCUS_NOT_INSTALLED = 3 + private const val MIN_DAYS_SINCE_FEEDBACK_PROMPT = 120 private fun Action.toInt() = when (this) { Action.BLOCKED -> BLOCKED_INT @@ -97,10 +99,20 @@ class Settings(private val appContext: Context) : PreferencesHolder { override val preferences: SharedPreferences = appContext.getSharedPreferences(FENIX_PREFERENCES, MODE_PRIVATE) - var useNewSearchExperience by featureFlagPreference( - appContext.getPreferenceKey(R.string.pref_key_use_new_search_experience), + var showTopFrecentSites by featureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_enable_top_frecent_sites), default = false, - featureFlag = FeatureFlags.newSearchExperience + featureFlag = FeatureFlags.topFrecentSite + ) + + var numberOfAppLaunches by intPreference( + appContext.getPreferenceKey(R.string.pref_key_times_app_opened), + default = 0 + ) + + var lastReviewPromptTimeInMillis by longPreference( + appContext.getPreferenceKey(R.string.pref_key_last_review_prompt_shown_time), + default = 0L ) var waitToShowPageUntilFirstPaint by featureFlagPreference( @@ -109,6 +121,12 @@ class Settings(private val appContext: Context) : PreferencesHolder { featureFlag = FeatureFlags.waitUntilPaintToDraw ) + var syncedTabsInTabsTray by featureFlagPreference( + appContext.getPreferenceKey(R.string.pref_key_synced_tabs_tabs_tray), + default = false, + featureFlag = FeatureFlags.syncedTabsInTabsTray + ) + var forceEnableZoom by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_accessibility_force_enable_zoom), default = false @@ -243,6 +261,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + var showCollectionsPlaceholderOnHome by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_show_collections_placeholder_home), + default = true + ) + val isCrashReportingEnabled: Boolean get() = isCrashReportEnabledInBuild && preferences.getBoolean( @@ -279,6 +302,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { !trackingProtectionOnboardingShownThisSession) var showSecretDebugMenuThisSession = false + var showNotificationsSetting = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O val shouldShowSecurityPinWarningSync: Boolean get() = loginsSecureWarningSyncCount < showLoginsSecureWarningSyncMaxCount @@ -822,6 +846,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = 0 ) + val topSitesMaxLimit by intPreference( + appContext.getPreferenceKey(R.string.pref_key_top_sites_max_limit), + default = topSitesMaxCount + ) + fun setOpenTabsCount(count: Int) { preferences.edit().putInt( appContext.getPreferenceKey(R.string.pref_key_open_tabs_count), diff --git a/app/src/main/java/org/mozilla/fenix/utils/Undo.kt b/app/src/main/java/org/mozilla/fenix/utils/Undo.kt index 6e8997770..543e2b257 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Undo.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Undo.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.utils import android.view.View import androidx.appcompat.widget.ContentFrameLayout +import androidx.core.view.updatePadding import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay @@ -70,11 +71,8 @@ fun CoroutineScope.allowUndo( val toolbarHeight = view.context.resources .getDimensionPixelSize(R.dimen.browser_toolbar_height) - snackbar.view.setPadding( - 0, - 0, - 0, - if ( + snackbar.view.updatePadding( + bottom = if ( paddedForBottomToolbar && shouldUseBottomToolbar && // If the view passed in is a ContentFrameLayout, it does not matter diff --git a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt index c9232ba8b..d76d4864d 100644 --- a/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/widget/VoiceSearchActivity.kt @@ -34,6 +34,11 @@ class VoiceSearchActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).resolveActivity(packageManager) == null) { + finish() + return + } + // Retrieve the previous intent from the saved state previousIntent = savedInstanceState?.get(PREVIOUS_INTENT) as Intent? if (previousIntent.isForSpeechProcessing()) { diff --git a/app/src/main/res/anim/full_rotation.xml b/app/src/main/res/anim/full_rotation.xml new file mode 100644 index 000000000..eefb51740 --- /dev/null +++ b/app/src/main/res/anim/full_rotation.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/color/pager_dot.xml b/app/src/main/res/color/pager_dot.xml new file mode 100644 index 000000000..c34fa0ffc --- /dev/null +++ b/app/src/main/res/color/pager_dot.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_download_default.xml b/app/src/main/res/drawable/ic_download_default.xml new file mode 100644 index 000000000..c7387d54b --- /dev/null +++ b/app/src/main/res/drawable/ic_download_default.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_type_apk.xml b/app/src/main/res/drawable/ic_file_type_apk.xml new file mode 100644 index 000000000..e68e42a68 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_apk.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_type_audio_note.xml b/app/src/main/res/drawable/ic_file_type_audio_note.xml new file mode 100644 index 000000000..0d1e65b73 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_audio_note.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_type_default.xml b/app/src/main/res/drawable/ic_file_type_default.xml new file mode 100644 index 000000000..f76b8b185 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_default.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_type_document.xml b/app/src/main/res/drawable/ic_file_type_document.xml new file mode 100644 index 000000000..5d1dd8325 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_document.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_file_type_image.xml b/app/src/main/res/drawable/ic_file_type_image.xml new file mode 100644 index 000000000..45fed07fe --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_image.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_file_type_video.xml b/app/src/main/res/drawable/ic_file_type_video.xml new file mode 100644 index 000000000..881a5c728 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_video.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_file_type_zip.xml b/app/src/main/res/drawable/ic_file_type_zip.xml new file mode 100644 index 000000000..75ad4adc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_type_zip.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_onboarding_avatar_anonymous_large.xml b/app/src/main/res/drawable/ic_onboarding_avatar_anonymous_large.xml new file mode 100644 index 000000000..422897fd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_onboarding_avatar_anonymous_large.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/pager_dot.xml b/app/src/main/res/drawable/pager_dot.xml new file mode 100644 index 000000000..869199def --- /dev/null +++ b/app/src/main/res/drawable/pager_dot.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/app/src/main/res/layout/browser_gesture_wrapper.xml b/app/src/main/res/layout/browser_gesture_wrapper.xml deleted file mode 100644 index 4de55e61f..000000000 --- a/app/src/main/res/layout/browser_gesture_wrapper.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/collection_header.xml b/app/src/main/res/layout/collection_header.xml index f78455a0e..e2d7af88d 100644 --- a/app/src/main/res/layout/collection_header.xml +++ b/app/src/main/res/layout/collection_header.xml @@ -6,7 +6,6 @@ android:id="@+id/collections_header_text" android:layout_width="wrap_content" android:layout_height="48dp" - android:layout_marginStart="4.5dp" android:gravity="center_vertical" android:text="@string/collections_header" android:textAppearance="@style/HeaderTextStyle" /> \ No newline at end of file diff --git a/app/src/main/res/layout/component_downloads.xml b/app/src/main/res/layout/component_downloads.xml new file mode 100644 index 000000000..a6bcbe35e --- /dev/null +++ b/app/src/main/res/layout/component_downloads.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_tabstray_fab.xml b/app/src/main/res/layout/component_tabstray_fab.xml index 8ea5ee9bb..2e9ac3a17 100644 --- a/app/src/main/res/layout/component_tabstray_fab.xml +++ b/app/src/main/res/layout/component_tabstray_fab.xml @@ -9,6 +9,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" + android:scrollbars="none" android:layout_margin="16dp" android:backgroundTint="@color/accent_normal_theme" android:contentDescription="@string/add_tab" diff --git a/app/src/main/res/layout/component_top_sites.xml b/app/src/main/res/layout/component_top_sites.xml index c0031d108..b9e0861f8 100644 --- a/app/src/main/res/layout/component_top_sites.xml +++ b/app/src/main/res/layout/component_top_sites.xml @@ -2,12 +2,19 @@ + diff --git a/app/src/main/res/layout/component_top_sites_pager.xml b/app/src/main/res/layout/component_top_sites_pager.xml new file mode 100644 index 000000000..f6f66d7fd --- /dev/null +++ b/app/src/main/res/layout/component_top_sites_pager.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/download_list_item.xml b/app/src/main/res/layout/download_list_item.xml new file mode 100644 index 000000000..7e9ddafd4 --- /dev/null +++ b/app/src/main/res/layout/download_list_item.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/about_libraries_activity.xml b/app/src/main/res/layout/fragment_about_libraries.xml similarity index 64% rename from app/src/main/res/layout/about_libraries_activity.xml rename to app/src/main/res/layout/fragment_about_libraries.xml index bf23bb437..58cea5d9d 100644 --- a/app/src/main/res/layout/about_libraries_activity.xml +++ b/app/src/main/res/layout/fragment_about_libraries.xml @@ -3,23 +3,16 @@ - 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/. --> - - + android:orientation="vertical" + tools:context="org.mozilla.fenix.settings.about.AboutLibrariesFragment"> - + diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 328f9bedd..09c23b747 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -2,52 +2,81 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent"> - - + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/loginSelectBar" + app:layout_constraintTop_toTopOf="parent" + tools:context="browser.BrowserFragment"> + - - - + + + + + + - + + + + + + + - - + diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml new file mode 100644 index 000000000..11fc15df2 --- /dev/null +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index cb7c95c3b..ffcb7ad2f 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -46,8 +46,8 @@ android:id="@+id/wordmark" android:layout_width="wrap_content" android:layout_height="40dp" - android:layout_marginStart="28dp" - android:layout_marginTop="56dp" + android:layout_marginStart="16dp" + android:layout_marginTop="18dp" android:layout_marginBottom="32dp" android:adjustViewBounds="true" android:clickable="false" diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index d85e21d64..6a7e8be88 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -63,7 +63,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:inflatedId="@id/search_suggestions_onboarding" - android:layout="@layout/search_suggestions_onboarding" + android:layout="@layout/search_suggestions_hint" app:layout_constraintBottom_toTopOf="@id/awesomeBar_barrier" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/fragment_search_dialog.xml b/app/src/main/res/layout/fragment_search_dialog.xml index 4a2abe039..6a6686f05 100644 --- a/app/src/main/res/layout/fragment_search_dialog.xml +++ b/app/src/main/res/layout/fragment_search_dialog.xml @@ -30,7 +30,7 @@ app:layout_constraintTop_toTopOf="parent"/> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/no_collections_message.xml b/app/src/main/res/layout/no_collections_message.xml index a37d36062..472259061 100644 --- a/app/src/main/res/layout/no_collections_message.xml +++ b/app/src/main/res/layout/no_collections_message.xml @@ -2,42 +2,63 @@ - + android:padding="16dp"> + app:fontFamily="@font/metropolis_semibold" + app:layout_constraintEnd_toStartOf="@id/remove_collection_placeholder" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/no_collections_header" /> - + android:visibility="gone" + app:icon="@drawable/ic_tab_collection" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/no_collections_description" /> + diff --git a/app/src/main/res/layout/onboarding_automatic_signin.xml b/app/src/main/res/layout/onboarding_automatic_signin.xml index d96855dcc..2b88fd85d 100644 --- a/app/src/main/res/layout/onboarding_automatic_signin.xml +++ b/app/src/main/res/layout/onboarding_automatic_signin.xml @@ -22,7 +22,7 @@ tools:text="@string/onboarding_firefox_account_auto_signin_header_2" />