diff --git a/.github/workflows/sync-strings.yml b/.github/workflows/sync-strings.yml new file mode 100644 index 000000000..35e1c5544 --- /dev/null +++ b/.github/workflows/sync-strings.yml @@ -0,0 +1,43 @@ +# 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/ + +name: "Sync Strings" + +on: + schedule: + - cron: '0 */4 * * *' + +jobs: + main: + name: "Sync Strings" + runs-on: ubuntu-20.04 + steps: + - name: "Discover Fenix Beta Version" + id: fenix-beta-version + uses: mozilla-mobile/fenix-beta-version@1.0.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: "Checkout Master Branch" + uses: actions/checkout@v2 + with: + path: main + ref: master + - name: "Checkout Beta Branch" + uses: actions/checkout@v2 + with: + path: beta + ref: "releases_v${{ steps.fenix-beta-version.outputs.fenix-beta-version }}.0.0" + - name: "Sync Strings" + uses: mozilla-mobile/sync-strings-action@1.0.1 + with: + src: main + dst: beta + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + path: beta + branch: automation/sync-strings-${{ steps.fenix-beta-version.outputs.major-beta-version }} + title: "Sync Strings from master to releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0" + body: "This (automated) PR syncs strings from `master` to `releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0.0`" diff --git a/.gitignore b/.gitignore index 67abe8cc2..cd0e1c137 100644 --- a/.gitignore +++ b/.gitignore @@ -80,7 +80,6 @@ gen-external-apklibs .DS_Store # Secrets files, e.g. tokens -.leanplum_token .adjust_token .sentry_token .mls_token diff --git a/app/build.gradle b/app/build.gradle index 635073f9a..5f5711313 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -255,7 +255,7 @@ android { // instead. :) maxParallelForks = 2 forkEvery = 80 - maxHeapSize = "2048m" + maxHeapSize = "3072m" minHeapSize = "1024m" } } @@ -362,25 +362,6 @@ android.applicationVariants.all { variant -> println("--") } -// ------------------------------------------------------------------------------------------------- -// Leanplum: Read token from local file if it exists -// ------------------------------------------------------------------------------------------------- - - print("Leanplum token: ") - - try { - def parts = new File("${rootDir}/.leanplum_token").text.trim().split(":") - def id = parts[0] - def key = parts[1] - buildConfigField 'String', 'LEANPLUM_ID', '"' + id + '"' - buildConfigField 'String', 'LEANPLUM_TOKEN', '"' + key + '"' - println "(Added from .leanplum_token file)" - } catch (FileNotFoundException ignored) { - buildConfigField 'String', 'LEANPLUM_ID', 'null' - buildConfigField 'String', 'LEANPLUM_TOKEN', 'null' - println("X_X") - } - // ------------------------------------------------------------------------------------------------- // MLS: Read token from local file if it exists // ------------------------------------------------------------------------------------------------- @@ -483,7 +464,6 @@ dependencies { implementation Deps.sentry - implementation Deps.mozilla_concept_base implementation Deps.mozilla_concept_engine implementation Deps.mozilla_concept_menu diff --git a/app/metrics.yaml b/app/metrics.yaml index 2e311c746..59f89331d 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -2,7 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. --- -$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 +$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0 no_lint: - CATEGORY_GENERIC @@ -11,6 +11,12 @@ events: app_opened_all_startup: type: event description: | + **This probe has a known flaw:** for COLD start up, it doesn't take into + account if the process is already running when the app starts, possibly + inflating results (e.g. a Service started the process 20min ago and only + now is HomeActivity launching). See the `cold_*_app_to_first_frame` probes + for a replacement. +

A user opened the app to the HomeActivity. The HomeActivity encompasses the home screen, browser screen, settings screen, collections and other screens in the nav_graph. @@ -217,6 +223,32 @@ events: notification_emails: - fenix-core@mozilla.com expires: "2021-07-01" + default_browser_changed: + type: event + description: | + Indicates the default browser was changed + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18857 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18895 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" + toolbar_menu_visible: + type: event + description: | + The browser menu was displayed from toolbar menu + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18855 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18895 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" total_uri_count: type: counter description: | @@ -555,6 +587,20 @@ search_shortcuts: - fenix-core@mozilla.com expires: "2021-08-01" +experiments_default_browser: + toolbar_menu_clicked: + type: event + description: | + Set default browser was clicked from toolbar menu + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18851 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18895 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" toolbar_settings: changed_position: type: event @@ -1187,6 +1233,40 @@ metrics: notification_emails: - fenix-core@mozilla.com expires: "2021-08-01" + start_reason_process_error: + type: boolean + description: | + The `AppStartReasonProvider.ProcessLifecycleObserver.onCreate` was + unexpectedly called twice. We can use this metric to validate our + assumptions about how these APIs are called. This probe can be removed + once we validate these assumptions. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18426 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" + start_reason_activity_error: + type: boolean + description: | + The `AppStartReasonProvider.ActivityLifecycleCallbacks.onActivityCreated` + was unexpectedly called twice. We can use this metric to validate our + assumptions about how these APIs are called. This probe can be removed + once we validate these assumptions. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18426 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" preferences: show_search_suggestions: @@ -2433,6 +2513,19 @@ tabs_tray: notification_emails: - fenix-core@mozilla.com expires: "2021-08-01" + synced_mode_tapped: + type: event + description: | + A user switched to synced mode + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18948 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/19004 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2022-08-01" new_tab_tapped: type: event description: | @@ -3938,6 +4031,23 @@ addons: notification_emails: - fenix-core@mozilla.com expires: "2021-07-01" + open_addon_setting: + type: event + description: | + A user opened an add-on's setting + extra_keys: + addon_id: + description: | + The id of the add-on that was interacted with + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17644 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18504 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2022-08-01" has_installed_addons: type: boolean description: | @@ -4126,6 +4236,95 @@ startup.timeline: expires: "2021-08-01" perf.startup: + cold_main_app_to_first_frame: + type: timing_distribution + time_unit: millisecond + description: | + The duration from `*Application`'s initializer to the first Android frame + being drawn in a [COLD MAIN start + up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary). + Notably, this duration omits the time from process start to the + initializer (which includes a lengthy dex operation) and the time from + the first frame to visual completeness. This probe doesn't measure Custom + Tabs or other uses of `ExternalAppBrowserActivity` to simplify result + analysis. The methodology for determining this measurement is imperfect + to simplify implementation. Issues may include: +
- Not measuring Beta and Release channels (due to + `MigrationDecisionActivity` interrupting the logic). +
- Not distinguishing between MAIN to homescreen, onboarding, session + restore, others? +
- Not choosing to record a MAIN based on what the user would see and + thus the core code path (i.e. the thing we want to measure) but rather on + the initial `Intent` state. +

+ The hope is that these cases will not have a significant impact on the end + results but, if they appear to, we can replace it with a more complex + implementation. +

+ Around April 8, 2021 the implementation was refactored. Functionally, it + should be the same but it's noted just in case there are bugs. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18426 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" + cold_view_app_to_first_frame: + type: timing_distribution + time_unit: millisecond + description: | + The duration from `*Application`'s initializer to the first Android frame + being drawn in a [COLD VIEW start + up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary). + The methodology for determining this measurement is imperfect to simplify + implementation. Issues may include: +
-Including VIEW intents that aren't valid so take code paths similar + to MAIN (this is speculative) +

+ See the `cold_main_app_to_first_frame` probe docs for other possible + known issues and more details. +

+ Around April 8, 2021 the implementation was refactored. Functionally, it + should be the same but it's noted just in case there are bugs. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18426 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" + cold_unknwn_app_to_first_frame: + type: timing_distribution + time_unit: millisecond + description: | + The duration from `*Application`'s initializer to the first Android frame + being drawn in a [COLD start + up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + where we can't say it was a MAIN or VIEW start up. The methodology for + determining this measurement is imperfect to simplify implementation. +

+ See the `cold_main_app_to_first_frame` probe docs for known issues and + more details. +

+ Around April 8, 2021 the implementation was refactored. Functionally, it + should be the same but it's noted just in case there are bugs. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18426 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" application_on_create: type: timing_distribution time_unit: millisecond @@ -4293,6 +4492,81 @@ perf.startup: - perf-android-fe@mozilla.com - mcomella@mozilla.com expires: "2021-08-11" + startup_type: + type: labeled_counter + description: | + Indicates how the browser was started. The label is divided into two + variables. `state` is how cached the browser is when started. `path` is + what code path we are expected to take. Together, they create a combined + label: `state_path`. For brevity, the specific states are documented in + the [Fenix perf + glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary). +

+ This implementation is intended to be simple, not comprehensive. We list + the implications below. + +

+ These ways of opening the app undesirably adds events to our primary + buckets (non-`unknown` cases): +
- App switcher cold/warm: `cold/warm_` + duplicates path from + previous launch +
- Home screen shortcuts: `*_view` +
- An Intent is sent internally that's uses `ACTION_MAIN` or + `ACTION_VIEW` could be: `*_main/view` (unknown if this ever happens) +
- A command-line launch uses `ACTION_MAIN` or `ACTION_VIEW` could be: + `*_main/view` + +

+ These ways of opening the app undesirably do not add their events to our + primary buckets: +
- Close and reopen the app very quickly: no event is recorded. + +

+ These ways of opening the app don't affect our primary buckets: +
- App switcher hot: `hot_unknown` +
- PWA (all states): `unknown_unknown` +
- Custom tab: `unknown_view` +
- Cold start where a service or other non-activity starts the process + (not manually tested) - this seems to happen if you have the homescreen + widget: `unknown_*` +
- Another activity is drawn before HomeActivity (e.g. widget voice + search): `unknown_*` +
- Widget text search: `*_unknown` + +

+ In addition to the events above, the `unknown` state may be chosen when we + were unable to determine a cause due to implementation details or the API + was used incorrectly. We may be able to record the events listed above + into different buckets but we kept the implementation simple for now. +

+ N.B.: for implementation simplicity, we duplicate the logic in app that + determines `path` so it's not perfectly accurate. In one way, we record we + is intended to happen rather than what actually happened (e.g. the user + may click a link so we record VIEW but the app does a MAIN by going to the + homescreen because the link was invalid). + labels: + - cold_main + - cold_view + - cold_unknown + - warm_main + - warm_view + - warm_unknown + - hot_main + - hot_view + - hot_unknown + - unknown_main + - unknown_view + - unknown_unknown + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18836 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/19028 + data_sensitivity: + - interaction + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-10-09" perf.awesomebar: history_suggestions: @@ -4312,7 +4586,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" bookmark_suggestions: send_in_pings: - metrics @@ -4330,7 +4604,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" search_engine_suggestions: send_in_pings: - metrics @@ -4348,7 +4622,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" session_suggestions: send_in_pings: - metrics @@ -4366,7 +4640,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" synced_tabs_suggestions: send_in_pings: - metrics @@ -4384,7 +4658,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" clipboard_suggestions: send_in_pings: - metrics @@ -4402,7 +4676,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" shortcuts_suggestions: send_in_pings: - metrics @@ -4420,7 +4694,7 @@ perf.awesomebar: notification_emails: - fenix-core@mozilla.com - gkruglov@mozilla.com - expires: "2020-11-15" + expires: "2021-11-15" autoplay: visited_setting: @@ -4811,6 +5085,46 @@ engine_tab: - fenix-core@mozilla.com - skaspari@mozilla.com expires: "2021-12-31" + foreground_metrics: + type: event + description: | + Event collecting data about the state of tabs when the app comes back to + the foreground. + bugs: + - https://github.com/mozilla-mobile/android-components/issues/9997 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + - skaspari@mozilla.com + expires: "2021-12-31" + extra_keys: + background_active_tabs: + description: | + Number of active tabs (with an engine session assigned) when the app + went to the background. + background_crashed_tabs: + description: | + Number of tabs marked as crashed when the app went to the background. + background_total_tabs: + description: | + Number of total tabs when the app went to the background. + foreground_active_tabs: + description: | + Number of active tabs (with an engine session assigned) when the + app came back to the foreground. + foreground_crashed_tabs: + description: | + Number of tabs marked as crashed when the app came back to the + foreground. + foreground_total_tabs: + description: | + Number of total tabs when the app came back to the foreground. + time_in_background: + description: | + Time (in milliseconds) the app was in the background. synced_tabs: synced_tabs_suggestion_clicked: @@ -4907,6 +5221,34 @@ awesomebar: - fenix-core@mozilla.com expires: "2021-08-01" +home_menu: + settings_item_clicked: + type: event + description: The user clicked the settings option in home menu. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18856 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18987 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" + +home_screen: + home_screen_displayed: + type: event + description: The user clicked the settings option in home menu. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18856 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/19025 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" + android_keystore_experiment: experiment_failure: type: event @@ -5007,3 +5349,46 @@ android_keystore_experiment: notification_emails: - fenix-core@mozilla.com expires: "2021-09-01" + +set_default_newtab_experiment: + set_default_browser_clicked: + type: event + description: | + Set default browser was clicked from new tab screen. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18853 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18895 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" + close_experiment_card_clicked: + type: event + description: | + Close experiment card was clicked from new tab screen. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18853 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18895 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" + +set_default_setting_experiment: + set_default_browser_clicked: + type: event + description: | + Set default browser was clicked from settings screen. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/18852 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/19047 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-10-01" diff --git a/app/pings.yaml b/app/pings.yaml index bd726ff12..d244af9f0 100644 --- a/app/pings.yaml +++ b/app/pings.yaml @@ -2,7 +2,7 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. --- -$schema: moz://mozilla.org/schemas/glean/pings/1-0-0 +$schema: moz://mozilla.org/schemas/glean/pings/2-0-0 activation: description: | diff --git a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt index 3575a7101..b40ad62b1 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt @@ -21,7 +21,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule // BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM. private const val EXPECTED_SUPPRESSION_COUNT = 11 -private const val EXPECTED_RUNBLOCKING_COUNT = 2 +private const val EXPECTED_RUNBLOCKING_COUNT = 3 private const val EXPECTED_COMPONENT_INIT_COUNT = 42 private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12 private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4 diff --git a/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt b/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt index d0f32270d..abedf0eaf 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt @@ -31,8 +31,8 @@ class DefaultHomeScreenTest : ScreenshotTest() { @Test fun showDefaultHomeScreen() { homeScreen { - verifyAccountsSignInButton() swipeToBottom() + verifyAccountsSignInButton() Screengrab.screenshot("HomeScreenRobot_home-screen-scroll") TestAssetHelper.waitingTime } diff --git a/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt b/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt index 3e3f2d674..d058bc8c7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/screenshots/MenuScreenShotTest.kt @@ -142,8 +142,8 @@ class MenuScreenShotTest : ScreenshotTest() { editBookmarkFolder() Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu") // It may be needed to wait here to have the screenshot - mDevice.pressBack() bookmarksMenu { + navigateUp() }.openThreeDotMenu("test") { deleteBookmarkFolder() Screengrab.screenshot("ThreeDotMenuBookmarksRobot_delete-bookmark-folder-menu") diff --git a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt index 7b6c02d16..2ab2cecca 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/SyncIntegrationTest.kt @@ -28,7 +28,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.ext.toUri import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.ui.robots.accountSettings -import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword @@ -141,10 +140,7 @@ class SyncIntegrationTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage) { }.openThreeDotMenu { - verifyAddBookmarkButton() - clickAddBookmarkButton() - } - browserScreen { + }.bookmarkPage { }.openThreeDotMenu { }.openSettings { }.openTurnOnSyncMenu { diff --git a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py index d4b325c00..37e03bee0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py +++ b/app/src/androidTest/java/org/mozilla/fenix/syncintegration/gradlewbuild.py @@ -21,7 +21,7 @@ class GradlewBuild(object): # Change path accordingly to go to root folder to run gradlew os.chdir('../../../../../../../..') - cmd = './gradlew ' + 'app:connectedGeckoNightlyDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}'.format(identifier) + cmd = './gradlew ' + 'app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}'.format(identifier) self.logger.info('Running cmd: {}'.format(cmd)) 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 e4893305b..d09de56b3 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -12,7 +12,6 @@ import mozilla.appservices.places.BookmarkRoot import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.R @@ -32,7 +31,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar /** * Tests for verifying basic functionality of bookmarks */ -@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979") class BookmarksTest { /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. @@ -96,10 +94,7 @@ class BookmarksTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddBookmarkButton() - clickAddBookmarkButton() - } - browserScreen { + }.bookmarkPage { }.openThreeDotMenu { verifyEditBookmarkButton() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt index 258a2a803..046055cfb 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -32,6 +32,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar * */ +@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/18421") class ContextMenusTest { private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private lateinit var mockWebServer: MockWebServer diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index ee42a73e6..4afff92dd 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -88,31 +88,11 @@ class NavigationToolbarTest { navigationToolbar { }.openThreeDotMenu { verifyThreeDotMenuExists() - verifyForwardButton() }.goForward { verifyUrl(nextWebPage.url.toString()) } } - @Test - fun refreshPageTest() { - val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer) - - navigationToolbar { - }.enterURLAndEnterToBrowser(refreshWebPage.url) { - mDevice.waitForIdle() - } - - // Use refresh from the three-dot menu - navigationToolbar { - }.openThreeDotMenu { - verifyThreeDotMenuExists() - verifyRefreshButton() - }.refreshPage { - verifyPageContent("REFRESHED") - } - } - @Test fun visitURLTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt index c1c2d582e..9373a195d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NoNetworkAccessStartupTests.kt @@ -78,7 +78,7 @@ class NoNetworkAccessStartupTests { browserScreen { }.openThreeDotMenu { - }.refreshPage {} + }.refreshPage { } } @Test diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt index e4b9ca94d..2d92884d6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt @@ -9,10 +9,8 @@ import androidx.test.espresso.IdlingRegistry import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityIntentTestRule @@ -104,7 +102,6 @@ class ReaderViewTest { @Test fun verifyReaderViewToggle() { - // New three-dot menu design does not have readerview appearance menu item val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -127,34 +124,21 @@ class ReaderViewTest { } browserScreen { verifyPageContent(estimatedReadingTime) - } + }.openThreeDotMenu { + verifyReaderViewAppearance(true) + }.closeBrowserMenuToBrowser { } + navigationToolbar { verifyCloseReaderViewDetected(true) toggleReaderView() mDevice.waitForIdle() verifyReaderViewDetected(true) - } - - if (!FeatureFlags.toolbarMenuFeature) { - browserScreen { - verifyPageContent(estimatedReadingTime) - }.openThreeDotMenu { - verifyReaderViewAppearance(true) - }.closeBrowserMenuToBrowser { } - } - - if (!FeatureFlags.toolbarMenuFeature) { - navigationToolbar { - toggleReaderView() - mDevice.waitForIdle() - }.openThreeDotMenu { - verifyReaderViewAppearance(false) - }.close { } - } + }.openThreeDotMenu { + verifyReaderViewAppearance(false) + }.close { } } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") fun verifyReaderViewAppearanceFontToggle() { val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -195,7 +179,6 @@ class ReaderViewTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") fun verifyReaderViewAppearanceFontSizeToggle() { val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -242,7 +225,6 @@ class ReaderViewTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") fun verifyReaderViewAppearanceColorSchemeChange() { val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) 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 a20877bd3..39191b4ec 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -106,7 +106,6 @@ class SettingsBasicsTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979") fun toggleShowVisitedSitesAndBookmarks() { // Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions. val page1 = getGenericAsset(mockWebServer, 1) @@ -117,15 +116,13 @@ class SettingsBasicsTest { }.openNavigationToolbar { }.enterURLAndEnterToBrowser(page1.url) { }.openThreeDotMenu { - clickAddBookmarkButton() - } + }.bookmarkPage { } navigationToolbar { }.enterURLAndEnterToBrowser(page2.url) { verifyUrl(page2.url.toString()) }.openThreeDotMenu { - clickAddBookmarkButton() - } + }.bookmarkPage { } navigationToolbar { }.enterURLAndEnterToBrowser(page3.url) { @@ -137,6 +134,7 @@ class SettingsBasicsTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/19016") @Test fun changeThemeSetting() { // Goes through the settings and changes the default search engine, then verifies it changes. @@ -163,6 +161,7 @@ class SettingsBasicsTest { } } + @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/18986") @Test fun changeAccessibiltySettings() { // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. 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 36c603a0d..81a1a4459 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsPrivacyTest.kt @@ -319,6 +319,7 @@ class SettingsPrivacyTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { + expandMenu() }.openAddToHomeScreen { addShortcutName(pageShortcutName) clickAddShortcutButton() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt index bc9767cdb..2329bde05 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ShareButtonTest.kt @@ -56,7 +56,6 @@ class ShareButtonTest { // From the 3-dot menu next to the Select share menu navigationToolbar { }.openThreeDotMenu { - verifyShareButton() clickShareButton() verifyShareScrim() verifySendToDeviceTitle() 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 3a5cd31ca..be94e034d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -17,6 +17,7 @@ import org.junit.Before import org.junit.Ignore import org.junit.Rule import org.junit.Test +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.AndroidAssetDispatcher @@ -35,12 +36,13 @@ import org.mozilla.fenix.ui.robots.enhancedTrackingProtection import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.tabDrawer +import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER /** * Test Suite that contains tests defined as part of the Smoke and Sanity check defined in Test rail. * These tests will verify different functionalities of the app as a way to quickly detect regressions in main areas */ - +@Suppress("ForbiddenComment") class SmokeTest { private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private lateinit var mockWebServer: MockWebServer @@ -125,13 +127,9 @@ class SmokeTest { verifyStartSyncHeader() verifyAccountsSignInButton() - // Intro to other sections - verifyGetToKnowHeader() - - // Automatic privacy - scrollToElementByText("Automatic privacy") + // Always-on privacy + scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) verifyAutomaticPrivacyHeader() - verifyTrackingProtectionToggle() verifyAutomaticPrivacyText() // Choose your theme @@ -142,11 +140,7 @@ class SmokeTest { verifyLightThemeDescription() verifyLightThemeToggle() - // Browse privately - verifyBrowsePrivatelyHeader() - verifyBrowsePrivatelyText() - - // Take a position + // Pick your toolbar placement verifyTakePositionHeader() verifyTakePositionElements() @@ -161,6 +155,7 @@ class SmokeTest { } @Test + @Ignore("https://github.com/mozilla-mobile/fenix/issues/18603") // Verifies the functionality of the onboarding Start Browsing button fun startBrowsingButtonTest() { homeScreen { @@ -199,14 +194,13 @@ class SmokeTest { @Test // Verifies the list of items in a tab's 3 dot menu - @Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870") fun verifyPageMainMenuItemsTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyThreeDotMainMenuItems() + verifyPageThreeDotMainMenuItems() } } @@ -233,12 +227,21 @@ class SmokeTest { } @Test - // Verifies the Synced tabs menu opens from a tab's 3 dot menu - fun openMainMenuSyncedTabsItemTest() { - homeScreen { - }.openThreeDotMenu { - }.openSyncedTabs { - verifySyncedTabsMenuHeader() + // Verifies the Synced tabs menu or Sync Sign In menu opens from a tab's 3 dot menu. + // The test is assuming we are NOT signed in. + fun openMainMenuSyncItemTest() { + if (FeatureFlags.tabsTrayRewrite) { + homeScreen { + }.openThreeDotMenu { + }.openSyncSignIn { + verifySyncSignInMenuHeader() + } + } else { + homeScreen { + }.openThreeDotMenu { + }.openSyncedTabs { + verifySyncedTabsMenuHeader() + } } } @@ -274,6 +277,7 @@ class SmokeTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { + expandMenu() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { @@ -292,12 +296,14 @@ class SmokeTest { }.openNavigationToolbar { }.enterURLAndEnterToBrowser(website.url) { }.openThreeDotMenu { + expandMenu() }.openAddToHomeScreen { clickCancelShortcutButton() } browserScreen { }.openThreeDotMenu { + expandMenu() }.openAddToHomeScreen { verifyShortcutNameField("Test_Page_1") addShortcutName("Test Page") @@ -322,7 +328,6 @@ class SmokeTest { @Test // Verifies the Bookmark button in a tab's 3 dot menu - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979") fun mainMenuBookmarkButtonTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -357,7 +362,6 @@ class SmokeTest { mDevice.waitForIdle() }.openThreeDotMenu { verifyThreeDotMenuExists() - verifyRefreshButton() }.refreshPage { verifyPageContent("REFRESHED") } @@ -365,7 +369,6 @@ class SmokeTest { @Test // Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar - @Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870") fun verifyETPShieldNotDisplayedIfOFFGlobally() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -392,6 +395,7 @@ class SmokeTest { } } + @Ignore("Failing, see https://github.com/mozilla-mobile/fenix/issues/18647") @Test fun customTrackingProtectionSettingsTest() { val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) @@ -542,7 +546,6 @@ class SmokeTest { @Test // Saves a login, then changes it and verifies the update - @Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870") fun updateSavedLoginTest() { val saveLoginTest = TestAssetHelper.getSaveLoginAsset(mockWebServer) @@ -606,7 +609,6 @@ class SmokeTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799") // Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers fun noCrashWithAddonInstalledTest() { // setting ETP to Strict mode to test it works with add-ons @@ -894,6 +896,7 @@ class SmokeTest { } } + @Ignore("Disabling until re-implemented by #19090") @Test fun verifyExpandedCollectionItemsTest() { val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -943,6 +946,7 @@ class SmokeTest { } } + @Ignore("Disabling until re-implemented by #19090") @Test fun shareCollectionTest() { val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -960,6 +964,7 @@ class SmokeTest { } } + @Ignore("Disabling until re-implemented by #19090") @Test fun deleteCollectionTest() { val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -981,7 +986,6 @@ class SmokeTest { @Test // Verifies that deleting a Bookmarks folder also removes the item from inside it. - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799") fun deleteNonEmptyBookmarkFolderTest() { val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -1058,6 +1062,7 @@ class SmokeTest { } } + @Ignore("Feature is temporarily removed; disabling test. See https://github.com/mozilla-mobile/fenix/issues/18656") @Test fun selectTabsButtonVisibilityTest() { homeScreen { @@ -1142,7 +1147,6 @@ class SmokeTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799") fun mainMenuInstallPWATest() { val pwaPage = "https://rpappalax.github.io/testapp/" @@ -1159,7 +1163,6 @@ class SmokeTest { } @Test - @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") // Verifies that reader mode is detected and the custom appearance controls are displayed fun verifyReaderViewAppearanceUI() { val readerViewPage = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt index 9e7494152..09ef8b83c 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt @@ -8,7 +8,6 @@ import androidx.test.platform.app.InstrumentationRegistry import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before -import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ext.settings @@ -127,7 +126,6 @@ class StrictEnhancedTrackingProtectionTest { } @Test - @Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870") fun testStrictVisitDisable() { val trackingProtectionTest = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) @@ -151,7 +149,6 @@ class StrictEnhancedTrackingProtectionTest { navigationToolbar { }.openThreeDotMenu { verifyThreeDotMenuExists() - verifySettingsButton() }.openSettings { verifyEnhancedTrackingProtectionButton() verifyEnhancedTrackingProtectionValue("On") 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 86645af12..69b2f294a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt @@ -4,12 +4,9 @@ package org.mozilla.fenix.ui -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.uiautomator.UiDevice import okhttp3.mockwebserver.MockWebServer import org.junit.After import org.junit.Before -import org.junit.BeforeClass import org.junit.Rule import org.junit.Test import org.mozilla.fenix.FeatureFlags @@ -38,85 +35,58 @@ class ThreeDotMenuMainTest { } } - // changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click - companion object { - @BeforeClass - @JvmStatic - fun setDevicePreference() { - val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - mDevice.executeShellCommand("settings put secure long_press_timeout 3000") - } - } - @After fun tearDown() { mockWebServer.shutdown() } + // Verifies the list of items in the homescreen's 3 dot main menu @Test - fun threeDotMenuItemsTest() { - if (FeatureFlags.toolbarMenuFeature) { - homeScreen { - }.openThreeDotMenu { - }.openHistory { - verifyHistoryMenuView() - }.goBackToBrowser {} - - homeScreen { - }.openThreeDotMenu { - }.openBookmarks { - verifyBookmarksMenuView() - }.closeMenu {} - - homeScreen { - }.openThreeDotMenu { - verifySettingsButton() - verifyBookmarksButton() - verifyHistoryButton() - }.openSettings { - verifySettingsView() - }.goBack { - }.openThreeDotMenu { - }.goBack {} - } else { - homeScreen { - }.openThreeDotMenu { - verifySettingsButton() - verifyBookmarksButton() - verifyHistoryButton() - verifyHelpButton() - verifyWhatsNewButton() - }.openSettings { - verifySettingsView() - }.goBack { - }.openThreeDotMenu { - }.openHelp { - verifyHelpUrl() - }.openTabDrawer { - closeTab() - } - - homeScreen { - }.openThreeDotMenu { - }.openWhatsNew { - verifyWhatsNewURL() - }.openTabDrawer { - closeTab() + fun homeThreeDotMenuItemsTest() { + homeScreen { + }.openThreeDotMenu { + verifyBookmarksButton() + verifyHistoryButton() + verifyDownloadsButton() + verifyAddOnsButton() + if (FeatureFlags.tabsTrayRewrite) { + verifySyncSignInButton() + } else { + verifySyncedTabsButton() } + verifyDesktopSite() + verifyWhatsNewButton() + verifyHelpButton() + verifySettingsButton() + }.openSettings { + verifySettingsView() + }.goBack { + }.openThreeDotMenu { + }.openHelp { + verifyHelpUrl() + }.openTabDrawer { + closeTab() + } - homeScreen { - }.openThreeDotMenu { - }.openBookmarks { - verifyBookmarksMenuView() - }.closeMenu { - } + homeScreen { + }.openThreeDotMenu { + }.openWhatsNew { + verifyWhatsNewURL() + }.openTabDrawer { + closeTab() + } - homeScreen { - }.openThreeDotMenu { - }.openHistory { - verifyHistoryMenuView() - } + homeScreen { + }.openThreeDotMenu { + }.openBookmarks { + verifyBookmarksMenuView() + }.closeMenu { } + homeScreen { + }.openThreeDotMenu { + }.openHistory { + verifyHistoryMenuView() + } } } 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 4f5a4bbc1..96eac6ace 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TopSitesTest.kt @@ -54,7 +54,8 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddFirefoxHome() + expandMenu() + verifyAddToTopSitesButton() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { @@ -73,7 +74,8 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddFirefoxHome() + expandMenu() + verifyAddToTopSitesButton() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { @@ -104,7 +106,8 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddFirefoxHome() + expandMenu() + verifyAddToTopSitesButton() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { @@ -128,7 +131,8 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddFirefoxHome() + expandMenu() + verifyAddToTopSitesButton() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { @@ -152,7 +156,8 @@ class TopSitesTest { navigationToolbar { }.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openThreeDotMenu { - verifyAddFirefoxHome() + expandMenu() + verifyAddToTopSitesButton() }.addToFirefoxHome { verifySnackBarText("Added to top sites!") }.openTabDrawer { 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 2e7343f36..d10a7df65 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 @@ -310,8 +310,7 @@ class BrowserRobot { // needs to wait for the right url to load before saving a bookmark verifyUrl(url.toString()) }.openThreeDotMenu { - clickAddBookmarkButton() - } + }.bookmarkPage { } } fun clickLinkMatchingText(expectedText: String) { 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 2db81496c..95825a1c5 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 @@ -54,6 +54,9 @@ import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.matchers.hasItem import org.mozilla.fenix.helpers.withBitmapDrawable +import org.mozilla.fenix.ui.util.STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER +import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER +import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER /** * Implementation of Robot Pattern for the home screen menu. @@ -85,7 +88,6 @@ class HomeScreenRobot { fun verifyStartSyncHeader() = assertStartSyncHeader() fun verifyAccountsSignInButton() = assertAccountsSignInButton() - fun verifyGetToKnowHeader() = assertGetToKnowHeader() fun verifyChooseThemeHeader() = assertChooseThemeHeader() fun verifyChooseThemeText() = assertChooseThemeText() fun verifyLightThemeToggle() = assertLightThemeToggle() @@ -95,18 +97,13 @@ class HomeScreenRobot { fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle() fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription() fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader() - fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle() - fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText() + fun verifyAutomaticPrivacyText() = assertAlwaysPrivacyText() - // Browse privately - fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader() - fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText() - - // Take a position - fun verifyTakePositionHeader() = assertTakePositionheader() + // Pick your toolbar placement + fun verifyTakePositionHeader() = assertTakePlacementHeader() fun verifyTakePositionElements() { - assertTakePositionBottomRadioButton() - assertTakePositionTopRadioButton() + assertTakePlacementBottomRadioButton() + assertTakePacementTopRadioButton() } // Your privacy @@ -549,18 +546,15 @@ private fun assertWelcomeHeader() = onView(allOf(withText("Welcome to ${appContext.appName}!"))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertStartSyncHeader() = - onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account."))) +private fun assertStartSyncHeader() { + scrollToElementByText(STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER) + onView(allOf(withText(R.string.onboarding_account_sign_in_header_1))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - +} private fun assertAccountsSignInButton() = onView(ViewMatchers.withResourceName("fxa_sign_in_button")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertGetToKnowHeader() = - onView(allOf(withText("Get to know ${appContext.appName}"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - private fun assertChooseThemeHeader() { scrollToElementByText("Choose your theme") onView(withText("Choose your theme")) @@ -568,7 +562,7 @@ private fun assertChooseThemeHeader() { } private fun assertChooseThemeText() { scrollToElementByText("Choose your theme") - onView(allOf(withText("Save some battery and your eyesight by enabling dark mode."))) + onView(allOf(withText("Save some battery and your eyesight with dark mode."))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } @@ -608,40 +602,23 @@ private fun assertAutomaticThemeDescription() { } private fun assertAutomaticPrivacyHeader() { - scrollToElementByText("Automatic privacy") - onView(allOf(withText("Automatic privacy"))) + scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) + onView(allOf(withText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun assertTrackingProtectionToggle() { - scrollToElementByText("Automatic privacy") - onView(withId(R.id.tracking_protection_toggle)) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertAutomaticPrivacyText() { - scrollToElementByText("Automatic privacy") +private fun assertAlwaysPrivacyText() { + scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER) onView( allOf( withText( - "Privacy and security settings block trackers, malware, and companies that follow you." + R.string.onboarding_tracking_protection_description_3 ) ) ) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun assertBrowsePrivatelyHeader() { - scrollToElementByText("Browse privately") - onView(allOf(withText("Browse privately"))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} - -private fun assertBrowsePrivatelyText() { - scrollToElementByText("Browse privately") - onView(allOf(withText(containsString("Update your private browsing settings.")))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -} private fun assertYourPrivacyHeader() { scrollToElementByText("Your privacy") onView(allOf(withText("Your privacy"))) @@ -668,25 +645,25 @@ private fun assertPrivacyNoticeButton() { private fun assertStartBrowsingButton() { scrollToElementByText("Start browsing") - onView(allOf(withText("Start browsing"))) + onView(withId(R.id.finish_button)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -// Take a position -private fun assertTakePositionheader() { - scrollToElementByText("Take a position") - onView(allOf(withText("Take a position"))) +// Pick your toolbar placement +private fun assertTakePlacementHeader() { + scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) + onView(allOf(withText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun assertTakePositionTopRadioButton() { - scrollToElementByText("Take a position") +private fun assertTakePacementTopRadioButton() { + scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) onView(ViewMatchers.withResourceName("toolbar_top_radio_button")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun assertTakePositionBottomRadioButton() { - scrollToElementByText("Take a position") +private fun assertTakePlacementBottomRadioButton() { + scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER) onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } 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 e6c3d13d2..c6177bc46 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 @@ -109,8 +109,7 @@ class NavigationToolbarRobot { withResourceName("onboarding_message"), // Req ETP dialog withResourceName("download_button") ) - ) - .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) + ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) } BrowserRobot().interact() @@ -186,10 +185,10 @@ class NavigationToolbarRobot { } fun openTabTray(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { - onView(withId(R.id.tab_button)) - .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) - + mDevice.waitForIdle(waitingTime) tabTrayButton().click() + mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")), + waitingTime) TabDrawerRobot().interact() return TabDrawerRobot.Transition() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt index e83a759f5..61acebf2a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsRobot.kt @@ -269,16 +269,20 @@ private fun assertThemeSelected() = onView(withText("Light")) private fun assertAccessibilityButton() = onView(withText("Accessibility")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertSetAsDefaultBrowserButton() = +private fun assertSetAsDefaultBrowserButton() { + scrollToElementByText("Set as default browser") onView(withText("Set as default browser")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} private fun assertDefaultBrowserIsDisabled() { + scrollToElementByText("Set as default browser") onView(withId(R.id.switch_widget)) .check(matches(ViewMatchers.isNotChecked())) } private fun toggleDefaultBrowserSwitch() { + scrollToElementByText("Set as default browser") onView( CoreMatchers.allOf( ViewMatchers.withParent(CoreMatchers.not(withId(R.id.navigationToolbar))), diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt index 07f36c0ff..c4dbca480 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt @@ -77,7 +77,7 @@ private fun assertDataCollectionOptions() { .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) val marketingDataText = - "Shares data about what features you use in Firefox Preview with Leanplum, our mobile marketing vendor." + "Shares basic usage data with Adjust, our mobile marketing vendor" onView(withText(marketingDataText)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt index 14e7b0a8e..e3fe7d6fa 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuEnhancedTrackingProtectionRobot.kt @@ -143,16 +143,13 @@ private fun assertEnhancedTrackingProtectionOptions() { onView(withText("Standard (default)")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - val stdText = "Blocks fewer trackers. Pages will load normally." - onView(withText(stdText)) + onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_standard_description_4)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Strict")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - val strictText = - "Blocks more trackers, ads, and popups. Pages load faster, but some functionality might not work." - onView(withText(strictText)) + onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_strict_description_3)) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withText("Custom")) @@ -168,16 +165,13 @@ private fun assertEnhancedTrackingProtectionOptionsGrayedOut() { onView(withText("Standard (default)")) .check(matches(not(isEnabled(true)))) - val stdText = "Blocks fewer trackers. Pages will load normally." - onView(withText(stdText)) + onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_standard_description_4)) .check(matches(not(isEnabled(true)))) onView(withText("Strict")) .check(matches(not(isEnabled(true)))) - val strictText = - "Blocks more trackers, ads, and popups. Pages load faster, but some functionality might not work." - onView(withText(strictText)) + onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_strict_description_3)) .check(matches(not(isEnabled(true)))) onView(withText("Custom")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt index dd129d479..566508837 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSitePermissionsCommonRobot.kt @@ -129,12 +129,19 @@ private fun assertVideoAndAudioBlockedRecommended() = onView(withId(R.id.fourth_ private fun assertCheckAutoPayRadioButtonDefault() { + // Allow audio and video onView(withId(R.id.block_radio)) .assertIsChecked(isChecked = false) + // Block audio and video on cellular data only + onView(withId(R.id.block_radio)) + .assertIsChecked(isChecked = false) + + // Block audio only onView(withId(R.id.third_radio)) .assertIsChecked(isChecked = false) + // Block audio and video onView(withId(R.id.fourth_radio)) .assertIsChecked(isChecked = true) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt new file mode 100644 index 000000000..ac7132ea7 --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncSignInRobot.kt @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public +* License, v. 2.0. If a copy of the MPL was not distributed with this +* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui.robots + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription +import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import org.hamcrest.CoreMatchers.allOf +import org.mozilla.fenix.R +import org.mozilla.fenix.helpers.click + +/** + * Implementation of Robot Pattern for Sync Sign In sub menu. + */ +class SyncSignInRobot { + + fun verifyAccountSettingsMenuHeader() = assertAccountSettingsMenuHeader() + fun verifySyncSignInMenuHeader() = assertSyncSignInMenuHeader() + + class Transition { + val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!! + + fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + goBackButton().click() + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + } +} + +private fun goBackButton() = + onView(allOf(withContentDescription("Navigate up"))) + +private fun assertAccountSettingsMenuHeader() { + // Replaced with the new string here, the test is assuming we are NOT signed in + // Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync + onView(withText(R.string.preferences_account_settings)) + .check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) +} + +private fun assertSyncSignInMenuHeader() { + onView(withText(R.string.sign_in_with_camera)) + .check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncedTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncedTabsRobot.kt index 9add0cd59..43aad7ccf 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncedTabsRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SyncedTabsRobot.kt @@ -39,6 +39,8 @@ private fun goBackButton() = onView(allOf(withContentDescription("Navigate up"))) private fun assertSyncedTabsMenuHeader() { - onView(withText(R.string.synced_tabs)) + // Replaced with the new string here, the test is assuming we are NOT signed in + // Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync + onView(withText(R.string.sync_menu_sign_in)) .check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) } 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 b1f2f647a..f0007760c 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 @@ -160,11 +160,29 @@ class TabDrawerRobot { fun clickTabMediaControlButton() = tabMediaControlButton().click() - fun clickSelectTabs() = onView(withText("Select tabs")).click() + fun clickSelectTabs() { + threeDotMenu().click() + + mDevice.waitNotNull( + Until.findObject(text("Select tabs")), + waitingTime + ) + + val selectTabsButton = mDevice.findObject(text("Select tabs")) + selectTabsButton.click() + } fun clickAddNewCollection() = addNewCollectionButton().click() - fun selectTab(title: String) = tab(title).click() + fun selectTab(title: String) { + mDevice.waitNotNull( + findObject(text(title)), + waitingTime + ) + + val tab = mDevice.findObject(text(title)) + tab.click() + } fun clickSaveCollection() = saveTabsToCollectionButton().click() @@ -198,15 +216,10 @@ class TabDrawerRobot { } fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { - mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/tab_button")) - .waitForExists(waitingTime) - + mDevice.waitForIdle(waitingTime) tabsCounter().click() - - org.mozilla.fenix.ui.robots.mDevice.waitNotNull( - Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")), - waitingTime - ) + mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")), + waitingTime) TabDrawerRobot().interact() return TabDrawerRobot.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 71d03cbc5..9db6eef94 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 @@ -6,24 +6,17 @@ package org.mozilla.fenix.ui.robots -import android.view.View import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.Espresso.pressBack -import androidx.test.espresso.UiController -import androidx.test.espresso.ViewAction import androidx.test.espresso.action.ViewActions -import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.swipeUp -import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility @@ -35,10 +28,9 @@ import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until -import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf +import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.click @@ -48,6 +40,7 @@ import org.mozilla.fenix.share.ShareFragment /** * Implementation of Robot Pattern for the three dot (main) menu. */ +@Suppress("ForbiddenComment") class ThreeDotMenuMainRobot { fun verifyTabSettingsButton() = assertTabSettingsButton() fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton() @@ -58,6 +51,7 @@ class ThreeDotMenuMainRobot { fun verifyHistoryButton() = assertHistoryButton() fun verifyBookmarksButton() = assertBookmarksButton() fun verifySyncedTabsButton() = assertSyncedTabsButton() + fun verifySyncSignInButton() = assertSignInToSyncButton() fun verifyHelpButton() = assertHelpButton() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyForwardButton() = assertForwardButton() @@ -68,7 +62,16 @@ class ThreeDotMenuMainRobot { fun verifyShareButton() = assertShareButton() fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) + fun expandMenu() { + onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp()) + } + fun clickShareButton() { + var maxSwipes = 3 + while (!shareButton().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } shareButton().click() mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) } @@ -85,73 +88,41 @@ class ThreeDotMenuMainRobot { addNewCollectionButton().click() } - fun clickAddBookmarkButton() { - mDevice.waitNotNull( - Until.findObject(By.desc("Bookmark")), - waitingTime - ) - addBookmarkButton().perform( - click( - /* no-op rollback action for when clicks randomly perform a long click, Espresso should attempt to click again - https://issuetracker.google.com/issues/37078920#comment9 - */ - object : ViewAction { - override fun getDescription(): String { - return "Handle tap->longclick." - } - - override fun getConstraints(): Matcher { - return isAssignableFrom(View::class.java) - } - - override fun perform(uiController: UiController?, view: View?) { - // do nothing - } - } - ) - ) - } - fun verifyCollectionNameTextField() = assertCollectionNameTextField() fun verifyFindInPageButton() = assertFindInPageButton() fun verifyShareScrim() = assertShareScrim() fun verifySendToDeviceTitle() = assertSendToDeviceTitle() fun verifyShareALinkTitle() = assertShareALinkTitle() fun verifyWhatsNewButton() = assertWhatsNewButton() - fun verifyAddFirefoxHome() = assertAddToFirefoxHome() + fun verifyAddToTopSitesButton() = assertAddToTopSitesButton() fun verifyAddToMobileHome() = assertAddToMobileHome() fun verifyDesktopSite() = assertDesktopSite() fun verifyDownloadsButton() = assertDownloadsButton() fun verifyShareTabsOverlay() = assertShareTabsOverlay() - - fun verifyThreeDotMainMenuItems() { - if (FeatureFlags.toolbarMenuFeature) { - verifyDownloadsButton() - verifyHistoryButton() - verifyBookmarksButton() - verifySettingsButton() - verifyDesktopSite() - verifySaveCollection() - verifyShareButton() - verifyForwardButton() - verifyRefreshButton() - } else { - verifyAddOnsButton() - verifyDownloadsButton() - verifyHistoryButton() - verifyBookmarksButton() - verifySyncedTabsButton() - verifySettingsButton() - verifyFindInPageButton() - verifyAddFirefoxHome() - verifyAddToMobileHome() - verifyDesktopSite() - verifySaveCollection() - verifyAddBookmarkButton() - verifyShareButton() - verifyForwardButton() - verifyRefreshButton() - } + fun verifySignInToSyncButton() = assertSignInToSyncButton() + fun verifyNewTabButton() = assertNewTabButton() + fun verifyReportSiteIssueButton() = assertReportSiteIssueButton() + + fun verifyPageThreeDotMainMenuItems() { + verifyNewTabButton() + verifyBookmarksButton() + verifyAddBookmarkButton() + verifyHistoryButton() + verifyDownloadsButton() + verifyAddOnsButton() + verifySignInToSyncButton() + threeDotMenuRecyclerView().perform(swipeUp()) + verifyFindInPageButton() + verifyDesktopSite() + threeDotMenuRecyclerView().perform(swipeUp()) + verifyReportSiteIssueButton() + verifyAddToTopSitesButton() + verifyAddToMobileHome() + verifySaveCollection() + verifySettingsButton() + verifyShareButton() + verifyForwardButton() + verifyRefreshButton() } private fun assertShareTabsOverlay() { @@ -166,11 +137,12 @@ class ThreeDotMenuMainRobot { private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { - onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) - onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings))) - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - .check(matches(isCompletelyDisplayed())) - .perform(ViewActions.click()) + var maxSwipes = 3 + while (!settingsButton().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } + settingsButton().click() SettingsRobot().interact() return SettingsRobot.Transition() @@ -185,7 +157,7 @@ class ThreeDotMenuMainRobot { } fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition { - onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) + onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime) syncedTabsButton().click() @@ -193,6 +165,15 @@ class ThreeDotMenuMainRobot { return SyncedTabsRobot.Transition() } + fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition { + onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) + mDevice.waitNotNull(Until.findObject(By.text("Sign in to sync")), waitingTime) + signInToSyncButton().click() + + SyncSignInRobot().interact() + return SyncSignInRobot.Transition() + } + fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition { onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) @@ -205,7 +186,7 @@ class ThreeDotMenuMainRobot { } fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition { - onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) + onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime) historyButton().click() @@ -214,7 +195,7 @@ class ThreeDotMenuMainRobot { } fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Bookmark")), waitingTime) + mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) addBookmarkButton().click() BrowserRobot().interact() @@ -222,10 +203,7 @@ class ThreeDotMenuMainRobot { } fun sharePage(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Share")), waitingTime) shareButton().click() - pressBack() - LibrarySubMenusMultipleSelectionToolbarRobot().interact() return LibrarySubMenusMultipleSelectionToolbarRobot.Transition() } @@ -239,7 +217,6 @@ class ThreeDotMenuMainRobot { } fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Forward")), waitingTime) forwardButton().click() BrowserRobot().interact() @@ -273,7 +250,7 @@ class ThreeDotMenuMainRobot { } fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - mDevice.waitNotNull(Until.findObject(By.desc("Refresh")), waitingTime) + assertRefreshButton() refreshButton().click() BrowserRobot().interact() @@ -303,7 +280,7 @@ class ThreeDotMenuMainRobot { } fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition { - onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) + onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime) findInPageButton().click() @@ -340,6 +317,11 @@ class ThreeDotMenuMainRobot { } fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition { + var maxSwipes = 3 + while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } readerViewAppearanceToggle().click() ReaderViewRobot().interact() @@ -347,7 +329,7 @@ class ThreeDotMenuMainRobot { } fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { - addToFirefoxHomeButton().click() + addToTopSitesButton().click() BrowserRobot().interact() return BrowserRobot.Transition() @@ -362,6 +344,11 @@ class ThreeDotMenuMainRobot { } fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { + var maxSwipes = 3 + while (!installPWAButton().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } installPWAButton().click() AddToHomeScreenRobot().interact() @@ -377,6 +364,11 @@ class ThreeDotMenuMainRobot { } fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { + // Ensure the menu is expanded and fully scrolled to the bottom. + for (i in 0..3) { + threeDotMenuRecyclerView().perform(swipeUp()) + } + mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) saveCollectionButton().click() ThreeDotMenuMainRobot().interact() @@ -401,18 +393,17 @@ class ThreeDotMenuMainRobot { } } } +private fun threeDotMenuRecyclerView() = + onView(withId(R.id.mozac_browser_menu_recyclerView)) private fun threeDotMenuRecyclerViewExists() { - onView(withId(R.id.mozac_browser_menu_recyclerView)).check(matches(isDisplayed())) + threeDotMenuRecyclerView().check(matches(isDisplayed())) } -private fun settingsButton() = onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings))) -private fun assertSettingsButton() = settingsButton() - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) - .check(matches(isCompletelyDisplayed())) +private fun settingsButton() = mDevice.findObject(UiSelector().text("Settings")) +private fun assertSettingsButton() = assertTrue(settingsButton().waitForExists(waitingTime)) -private val addOnsText = if (FeatureFlags.toolbarMenuFeature) "Extensions" else "Add-ons" -private fun addOnsButton() = onView(allOf(withText(addOnsText))) +private fun addOnsButton() = onView(allOf(withText("Add-ons"))) private fun assertAddOnsButton() { onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) @@ -430,27 +421,28 @@ private fun syncedTabsButton() = onView(allOf(withText(R.string.library_synced_t private fun assertSyncedTabsButton() = syncedTabsButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun signInToSyncButton() = onView(withText("Sign in to sync")) +private fun assertSignInToSyncButton() = signInToSyncButton().check(matches(isDisplayed())) + private fun helpButton() = onView(allOf(withText(R.string.browser_menu_help))) private fun assertHelpButton() = helpButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forward")) -private fun assertForwardButton() = forwardButton() - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward")) +private fun assertForwardButton() = assertTrue(forwardButton().waitForExists(waitingTime)) -private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark")) +private fun addBookmarkButton() = onView(allOf(withId(R.id.checkbox), withText("Add"))) private fun assertAddBookmarkButton() { onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp()) addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark")) +private fun editBookmarkButton() = onView(withText("Edit")) private fun assertEditBookmarkButton() = editBookmarkButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh")) -private fun assertRefreshButton() = refreshButton() - .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh")) +private fun assertRefreshButton() = assertTrue(refreshButton().waitForExists(waitingTime)) private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop")) @@ -462,14 +454,13 @@ private fun shareTabButton() = onView(allOf(withText("Share all tabs"))).inRoot( private fun assertShareTabButton() = shareTabButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun shareButton() = onView(ViewMatchers.withContentDescription("Share")) -private fun assertShareButton() = shareButton() - .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) +private fun shareButton() = mDevice.findObject(UiSelector().description("Share")) +private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime)) private fun browserViewSaveCollectionButton() = onView( allOf( withText("Save to collection"), - withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) + withEffectiveVisibility(Visibility.VISIBLE) ) ) @@ -492,11 +483,14 @@ private fun assertCollectionNameTextField() = collectionNameTextField() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) private fun reportSiteIssueButton() = onView(withText("Report Site Issue…")) +private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed())) private fun findInPageButton() = onView(allOf(withText("Find in page"))) + private fun assertFindInPageButton() = findInPageButton() private fun shareScrim() = onView(withResourceName("closeSharingScrim")) + private fun assertShareScrim() = shareScrim().check(matches(ViewMatchers.withAlpha(ShareFragment.SHOW_PAGE_ALPHA))) @@ -524,17 +518,29 @@ private fun assertWhatsNewButton() = whatsNewButton() private fun addToHomeScreenButton() = onView(withText("Add to Home screen")) private fun readerViewAppearanceToggle() = - onView(allOf(withText(R.string.browser_menu_read_appearance))) - -private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppearanceToggle() - .check( - if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE)) else ViewAssertions.doesNotExist() - ) + mDevice.findObject(UiSelector().text("Customize reader view")) + +private fun assertReaderViewAppearanceButton(visible: Boolean) { + var maxSwipes = 3 + if (visible) { + while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } + assertTrue(readerViewAppearanceToggle().exists()) + } else { + while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) { + threeDotMenuRecyclerView().perform(swipeUp()) + maxSwipes-- + } + assertFalse(readerViewAppearanceToggle().exists()) + } +} -private fun addToFirefoxHomeButton() = +private fun addToTopSitesButton() = onView(allOf(withText(R.string.browser_menu_add_to_top_sites))) -private fun assertAddToFirefoxHome() { +private fun assertAddToTopSitesButton() { onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( RecyclerViewActions.scrollTo( @@ -545,6 +551,7 @@ private fun assertAddToFirefoxHome() { private fun addToMobileHomeButton() = onView(allOf(withText(R.string.browser_menu_add_to_homescreen))) + private fun assertAddToMobileHome() { onView(withId(R.id.mozac_browser_menu_recyclerView)) .perform( @@ -554,7 +561,7 @@ private fun assertAddToMobileHome() { ).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) } -private fun installPWAButton() = onView(allOf(withId(R.id.highlight_text), withText("Install"))) +private fun installPWAButton() = mDevice.findObject(UiSelector().text("Install")) private fun desktopSiteButton() = onView(allOf(withText(R.string.browser_menu_desktop_site))) @@ -602,3 +609,5 @@ private fun assertShareAllTabsButton() { .check( matches(isDisplayed())) } + +private fun assertNewTabButton() = onView(withText("New tab")).check(matches(isDisplayed())) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt new file mode 100644 index 000000000..3550cf1bc --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/util/Strings.kt @@ -0,0 +1,9 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.ui.util + +const val STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER = "Sync Firefox between devices" +const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Always-on privacy" +const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement" diff --git a/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt b/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt index 2c58b165a..4fbd61291 100644 --- a/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoBeta/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -83,6 +83,7 @@ object GeckoProvider { val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val loginStorageDelegate = GeckoLoginStorageDelegate(storage) + @Suppress("Deprecation") geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) return geckoRuntime diff --git a/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt b/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt index 38c751f30..9a1d2f8b1 100644 --- a/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoNightly/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -83,6 +83,7 @@ object GeckoProvider { val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val loginStorageDelegate = GeckoLoginStorageDelegate(storage) + @Suppress("Deprecation") geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) return geckoRuntime diff --git a/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt b/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt index 77303e932..15e3d01e3 100644 --- a/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -93,6 +93,7 @@ object GeckoProvider { val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val loginStorageDelegate = GeckoLoginStorageDelegate(storage) + @Suppress("Deprecation") geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) return geckoRuntime diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b13db0148..a91e307b2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,7 @@ - navController?.get()?.navigate( + navController?.get()?.navigateBlockingForAsyncNavGraph( NavGraphDirections.actionGlobalAddonsManagementFragment(addonId) ) diff --git a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt index e14c6c048..6b3734e59 100644 --- a/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt +++ b/app/src/main/java/org/mozilla/fenix/BrowserDirection.kt @@ -31,6 +31,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) { FromAddonDetailsFragment(R.id.addonDetailsFragment), FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment), FromLoginDetailFragment(R.id.loginDetailFragment), - FromTabTray(R.id.tabTrayDialogFragment), + FromTabTrayDialog(R.id.tabTrayDialogFragment), + FromTabTray(R.id.tabsTrayFragment), FromRecentlyClosed(R.id.recentlyClosedFragment) } diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index dfeffd860..ec81c9482 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -37,15 +37,10 @@ object FeatureFlags { /** * Shows new three-dot toolbar menu design. */ - val toolbarMenuFeature = Config.channel.isDebug + const val toolbarMenuFeature = true /** * Enables the tabs tray re-write with Synced Tabs. */ - val tabsTrayRewrite = Config.channel.isDebug - - /** - * Enables the updated icon set look and feel. - */ - val newIconSet = Config.channel.isNightlyOrDebug + const val tabsTrayRewrite = true } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 837ccbf95..a14b00e73 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -13,6 +13,7 @@ import androidx.annotation.CallSuper import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AppCompatDelegate import androidx.core.content.getSystemService +import androidx.lifecycle.ProcessLifecycleOwner import androidx.work.Configuration.Builder import androidx.work.Configuration.Provider import kotlinx.coroutines.Deferred @@ -59,6 +60,7 @@ import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.WebPushEngineIntegration import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.VisibilityLifecycleCallback +import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver import org.mozilla.fenix.utils.BrowsersCache import java.util.concurrent.TimeUnit @@ -190,8 +192,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // runStorageMaintenance() // } + components.appStartReasonProvider.registerInAppOnCreate(this) + components.startupActivityLog.registerInAppOnCreate(this) initVisualCompletenessQueueAndQueueTasks() + ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store)) + components.appStartupTelemetry.onFenixApplicationOnCreate() } } diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index c85a39d76..1da6a183f 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -16,6 +16,7 @@ import android.util.AttributeSet import android.view.KeyEvent import android.view.LayoutInflater import android.view.View +import android.view.ActionMode import android.view.ViewConfiguration import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.annotation.CallSuper @@ -32,13 +33,14 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.NavigationUI import kotlinx.android.synthetic.main.activity_home.* import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.Dispatchers.IO import mozilla.appservices.places.BookmarkRoot +import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.state.SessionState @@ -77,13 +79,13 @@ import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExcepti import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor -import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor @@ -92,10 +94,13 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections +import org.mozilla.fenix.perf.NavGraphProvider import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.ProfilerMarkers +import org.mozilla.fenix.perf.StartupPathProvider import org.mozilla.fenix.perf.StartupTimeline +import org.mozilla.fenix.perf.StartupTypeTelemetry import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.settings.SettingsFragmentDirections @@ -107,6 +112,7 @@ import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections +import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections import org.mozilla.fenix.theme.DefaultThemeManager @@ -121,7 +127,7 @@ import java.lang.ref.WeakReference * - browser screen */ @OptIn(ExperimentalCoroutinesApi::class) -@SuppressWarnings("TooManyFunctions", "LargeClass") +@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList") open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL // we need to store startup timestamp for warm startup. we cant directly store @@ -129,7 +135,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // components requires context to access. protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos() - private var webExtScope: CoroutineScope? = null lateinit var themeManager: ThemeManager lateinit var browsingModeManager: BrowsingModeManager @@ -154,7 +159,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { listOf( SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics), StartSearchIntentProcessor(components.analytics.metrics), - DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService), OpenBrowserIntentProcessor(this, ::getIntentSessionId), OpenSpecificTabIntentProcessor(this) ) @@ -165,6 +169,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private lateinit var navigationToolbar: Toolbar + // Tracker for contextual menu (Copy|Search|Select all|etc...) + private var actionMode: ActionMode? = null + + private val startupPathProvider = StartupPathProvider() + private lateinit var startupTypeTelemetry: StartupTypeTelemetry + final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measureNoInline { // DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL. components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity") @@ -172,6 +182,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager) // There is disk read violations on some devices such as samsung and pixel for android 9/10 components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { + // Theme setup should always be called before super.onCreate + setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) super.onCreate(savedInstanceState) } @@ -187,8 +199,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { components.publicSuffixList.prefetch() - setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) - setContentView(R.layout.activity_home) + setContentView(R.layout.activity_home).run { + // Do not call anything between setContentView and inflateNavGraphAsync. + // It needs to start its job as early as possible. + NavGraphProvider.inflateNavGraphAsync(navHost.navController, lifecycleScope) + } // Must be after we set the content view if (isVisuallyComplete) { @@ -204,17 +219,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { it.start() } - if (isActivityColdStarted( - intent, - savedInstanceState - ) && !externalSourceIntentProcessors.any { - it.process( - intent, - navHost.navController, - this.intent - ) - } - ) { + if (isActivityColdStarted(intent, savedInstanceState) && + !externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }) { navigateToBrowserOnColdStart() } @@ -252,6 +258,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { captureSnapshotTelemetryMetrics() startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null) + startupPathProvider.attachOnActivityOnCreate(lifecycle, intent) + startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply { + attachOnHomeActivityOnCreate(lifecycle) + } components.core.requestInterceptor.setNavigationController(navHost.navController) @@ -262,11 +272,19 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { safeIntent: SafeIntent, hasSavedInstanceState: Boolean ) { + // This function gets overridden by subclasses. components.appStartupTelemetry.onHomeActivityOnCreate( safeIntent, hasSavedInstanceState, homeActivityInitTimeStampNanoSeconds, rootContainer ) + + components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate( + components.performance.visualCompletenessQueue, + components.startupStateProvider, + safeIntent, + rootContainer + ) } override fun onRestart() { @@ -455,6 +473,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { intent?.let { handleNewIntent(it) } + startupPathProvider.onIntentReceived(intent) } open fun handleNewIntent(intent: Intent) { @@ -519,6 +538,20 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { else -> super.onCreateView(parent, name, context, attrs) } + override fun onActionModeStarted(mode: ActionMode?) { + actionMode = mode + super.onActionModeStarted(mode) + } + + override fun onActionModeFinished(mode: ActionMode?) { + actionMode = null + super.onActionModeFinished(mode) + } + + fun finishActionMode() { + actionMode?.finish().also { actionMode = null } + } + @Suppress("MagicNumber") // Defining the positions as constants doesn't seem super useful here. private fun actionSorter(actions: Array): Array { @@ -721,10 +754,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { customTabSessionId: String? = null, engine: SearchEngine? = null, forceSearch: Boolean = false, - flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none() + flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), + requestDesktopMode: Boolean = false ) { openToBrowser(from, customTabSessionId) - load(searchTermOrURL, newTab, engine, forceSearch, flags) + load(searchTermOrURL, newTab, engine, forceSearch, flags, requestDesktopMode) } fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) { @@ -774,8 +808,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromLoginDetailFragment -> LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) - BrowserDirection.FromTabTray -> + BrowserDirection.FromTabTrayDialog -> TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) + BrowserDirection.FromTabTray -> + TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId) BrowserDirection.FromRecentlyClosed -> RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId) } @@ -790,7 +826,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { newTab: Boolean, engine: SearchEngine?, forceSearch: Boolean, - flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none() + flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(), + requestDesktopMode: Boolean = false ) { val startTime = components.core.engine.profiler?.getProfilerTime() val mode = browsingModeManager.mode @@ -807,6 +844,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { // and let it try to load whatever was entered. if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) { loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags) + + if (requestDesktopMode) { + handleRequestDesktopMode() + } } else { if (newTab) { components.useCases.searchUseCases.newTabSearch @@ -833,6 +874,19 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } } + internal fun handleRequestDesktopMode() { + val requestDesktopSiteUseCase = + components.useCases.sessionUseCases.requestDesktopSite + requestDesktopSiteUseCase.invoke(true) + components.core.store.dispatch( + ContentAction.UpdateDesktopModeAction( + components.core.store.state.selectedTabId.toString(), true + ) + ) + // Reset preference value after opening the tab in desktop mode + settings().openNextTabInDesktopMode = false + } + open fun navigateToBrowserOnColdStart() { // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate @@ -850,7 +904,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { } override fun getSystemService(name: String): Any? { - if (LAYOUT_INFLATER_SERVICE == name) { + // Issue #17759 had a crash with the PerformanceInflater.kt on Android 5.0 and 5.1 + // when using the TimePicker. Since the inflater was created for performance monitoring + // purposes and that we test on new android versions, this means that any difference in + // inflation will be caught on those devices. + if (LAYOUT_INFLATER_SERVICE == name && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { if (inflater == null) { inflater = PerformanceInflater(LayoutInflater.from(baseContext), this) } @@ -885,7 +943,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { webExtensionId = webExtensionState.id, webExtensionTitle = webExtensionState.name ) - navHost.navController.navigate(action) + navHost.navController.navigateBlockingForAsyncNavGraph(action) } /** 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 64c77733d..ed7780177 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -358,7 +358,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) adapter?.updateAddon(it) addonProgressOverlay?.visibility = View.GONE showInstallationDialog(it) - Addons.hasInstalledAddons.set(true) } }, onError = { _, e -> diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt index 4e9e24cab..d1994c78a 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementView.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavController import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate import org.mozilla.fenix.R +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.navigateSafe /** @@ -55,6 +56,6 @@ class AddonsManagementView( AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment( unsupportedAddons.toTypedArray() ) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt index c154aa4f9..cb550e2b5 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt @@ -21,10 +21,11 @@ import kotlinx.coroutines.launch import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.ui.translateName -import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.runIfFragmentIsAttached @@ -128,7 +129,6 @@ class InstalledAddonDetailsFragment : Fragment() { ) ) } - Addons.hasEnabledAddons.set(true) } }, onError = { @@ -196,6 +196,9 @@ class InstalledAddonDetailsFragment : Fragment() { view.settings.apply { isVisible = shouldSettingsBeVisible() setOnClickListener { + requireContext().components.analytics.metrics.track( + Event.AddonOpenSetting(addon.id) + ) val settingUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener val directions = if (addon.installedState?.openOptionsPageInTab == true) { val components = it.context.components @@ -213,7 +216,7 @@ class InstalledAddonDetailsFragment : Fragment() { InstalledAddonDetailsFragmentDirections .actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon) } - Navigation.findNavController(this).navigate(directions) + Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions) } } } @@ -224,7 +227,7 @@ class InstalledAddonDetailsFragment : Fragment() { InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment( addon ) - Navigation.findNavController(view).navigate(directions) + Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions) } } @@ -234,7 +237,7 @@ class InstalledAddonDetailsFragment : Fragment() { InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment( addon ) - Navigation.findNavController(view).navigate(directions) + Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/android/DefaultActivityLifecycleCallbacks.kt b/app/src/main/java/org/mozilla/fenix/android/DefaultActivityLifecycleCallbacks.kt new file mode 100644 index 000000000..a47480965 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/android/DefaultActivityLifecycleCallbacks.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.android + +import android.app.Activity +import android.app.Application +import android.os.Bundle + +/** + * An inheritance of [Application.ActivityLifecycleCallbacks] where each method has a default + * implementation that does nothing. This allows classes that extend this interface to have + * more concise definitions if they don't implement some methods; this is in the spirit of + * other `Default*` classes, such as [androidx.lifecycle.DefaultLifecycleObserver]. + */ +interface DefaultActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, bundle: Bundle?) {} + override fun onActivityStarted(activity: Activity) {} + override fun onActivityResumed(activity: Activity) {} + override fun onActivityPaused(activity: Activity) {} + override fun onActivityStopped(activity: Activity) {} + override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {} + override fun onActivityDestroyed(activity: Activity) {} +} 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 29b2ec9af..0307b146a 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -80,6 +80,7 @@ import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged @@ -127,9 +128,9 @@ import java.lang.ref.WeakReference import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior import mozilla.components.feature.webauthn.WebAuthnFeature import mozilla.components.support.base.feature.ActivityResultHandler +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import mozilla.components.support.ktx.android.view.enterToImmersiveMode import org.mozilla.fenix.GleanMetrics.PerfStartup -import org.mozilla.fenix.ext.enterToImmersiveMode -import org.mozilla.fenix.ext.exitImmersiveModeIfNeeded import org.mozilla.fenix.ext.measureNoInline import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition @@ -282,7 +283,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit val readerMenuController = DefaultReaderModeController( readerViewFeature, view.readerViewControlsBar, - isPrivate = activity.browsingModeManager.mode.isPrivate + isPrivate = activity.browsingModeManager.mode.isPrivate, + onReaderModeChanged = { activity.finishActionMode() } ) val browserToolbarController = DefaultBrowserToolbarController( store = store, @@ -370,8 +372,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit store = store, sessionId = customTabSessionId, stub = view.stubFindInPage, - engineView = view.engineView, - toolbar = browserToolbarView.view + engineView = engineView, + toolbarInfo = FindInPageIntegration.ToolbarInfo( + browserToolbarView.view, + !context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled, + !context.settings().shouldUseBottomToolbar + ) ), owner = this, view = view @@ -552,7 +558,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit showPage = true, sessionId = getCurrentTab()?.id ) - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } }, onNeedToRequestPermissions = { permissions -> @@ -563,7 +569,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit browserAnimator.captureEngineViewAndDrawStatically { val directions = NavGraphDirections.actionGlobalSavedLoginsAuthFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } } ), @@ -851,12 +857,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit view: View ): List - @CallSuper - override fun onStart() { - super.onStart() - sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener() - } - @VisibleForTesting internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) { val activity = activity as HomeActivity @@ -980,7 +980,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit } override fun onBackLongPressed(): Boolean { - findNavController().navigate( + findNavController().navigateBlockingForAsyncNavGraph( NavGraphDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId ) 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 58515a908..ac70ff92c 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -33,6 +33,7 @@ import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.requireComponents @@ -264,7 +265,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { ) .setText(view.context.getString(messageStringRes)) .setAction(requireContext().getString(R.string.create_collection_view)) { - findNavController().navigate( + findNavController().navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false) ) } diff --git a/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt index 1d1ba4041..700803141 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt @@ -33,8 +33,10 @@ open class InfoBanner( private val dismissText: String, private val actionText: String? = null, private val dismissByHiding: Boolean = false, - private val dismissAction: (() -> Unit)? = null, - private val actionToPerform: (() -> Unit)? = null + @VisibleForTesting + internal val dismissAction: (() -> Unit)? = null, + @VisibleForTesting + internal val actionToPerform: (() -> Unit)? = null ) { @SuppressLint("InflateParams") @VisibleForTesting diff --git a/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt b/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt index 478f5dc89..bc24e7dfb 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/readermode/ReaderModeController.kt @@ -24,9 +24,11 @@ interface ReaderModeController { class DefaultReaderModeController( private val readerViewFeature: ViewBoundFeatureWrapper, private val readerViewControlsBar: View, - private val isPrivate: Boolean = false + private val isPrivate: Boolean = false, + private val onReaderModeChanged: () -> Unit = {} ) : ReaderModeController { override fun hideReaderView() { + onReaderModeChanged() readerViewFeature.withFeature { it.hideReaderView() it.hideControls() @@ -34,6 +36,7 @@ class DefaultReaderModeController( } override fun showReaderView() { + onReaderModeChanged() readerViewFeature.withFeature { it.showReaderView() } } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt new file mode 100644 index 000000000..a5c6a8ba8 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionsDialog.kt @@ -0,0 +1,135 @@ +/* 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.collections + +import android.content.Context +import android.view.LayoutInflater +import android.widget.EditText +import androidx.appcompat.app.AlertDialog +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.support.ktx.android.view.showKeyboard +import org.mozilla.fenix.R +import org.mozilla.fenix.components.TabCollectionStorage +import org.mozilla.fenix.ext.getDefaultCollectionNumber + +/** + * A lambda that is invoked when a confirmation button in a [CollectionsDialog] is clicked. + * + * A [TabCollection] of the selected collected is passed to the delegate when confirmed. If null, + * then a new collection is created. + * + * A list of [TabSessionState] is returned that will be put into the collections storage. + */ +typealias OnPositiveButtonClick = (collection: TabCollection?) -> List + +/** + * A lambda that is invoked when a cancel button in a [CollectionsDialog] is clicked. + */ +typealias OnNegativeButtonClick = () -> Unit + +/** + * A data class for creating a dialog to prompt adding/creating a collection. See also [show]. + * + * @property onPositiveButtonClick Invoked when a user clicks on a confirmation button in the dialog. + * @property onNegativeButtonClick Invoked when a user clicks on a cancel button in the dialog. + */ +data class CollectionsDialog( + val storage: TabCollectionStorage, + val onPositiveButtonClick: OnPositiveButtonClick, + val onNegativeButtonClick: OnNegativeButtonClick +) + +/** + * Create and display a [CollectionsDialog] using [AlertDialog]. + */ +fun CollectionsDialog.show( + context: Context +) { + if (storage.cachedTabCollections.isEmpty()) { + showAddNewDialog(context, storage) + return + } + + val collections = storage.cachedTabCollections.map { it.title } + val layout = LayoutInflater.from(context).inflate(R.layout.add_new_collection_dialog, null) + val list = layout.findViewById(R.id.recycler_view) + + val builder = AlertDialog.Builder(context).setTitle(R.string.tab_tray_select_collection) + .setView(layout) + .setPositiveButton(android.R.string.ok) { dialog, _ -> + val selectedCollection = + (list.adapter as CollectionsListAdapter).getSelectedCollection() + val collection = storage.cachedTabCollections[selectedCollection] + val sessionList = onPositiveButtonClick.invoke(collection) + + MainScope().launch { + storage.addTabsToCollection(collection, sessionList) + } + + dialog.dismiss() + }.setNegativeButton(android.R.string.cancel) { dialog, _ -> + onNegativeButtonClick.invoke() + + dialog.cancel() + } + + val dialog = builder.create() + val collectionNames = + arrayOf(context.getString(R.string.tab_tray_add_new_collection)) + collections + val collectionsListAdapter = CollectionsListAdapter(collectionNames) { + dialog.dismiss() + showAddNewDialog(context, storage) + } + + list.apply { + layoutManager = LinearLayoutManager(context) + adapter = collectionsListAdapter + } + dialog.show() +} + +internal fun CollectionsDialog.showAddNewDialog( + context: Context, + collectionsStorage: TabCollectionStorage +) { + val layout = LayoutInflater.from(context).inflate(R.layout.name_collection_dialog, null) + val collectionNameEditText: EditText = layout.findViewById(R.id.collection_name) + + collectionNameEditText.setText( + context.getString( + R.string.create_collection_default_name, + collectionsStorage.cachedTabCollections.getDefaultCollectionNumber() + ) + ) + + AlertDialog.Builder(context) + .setTitle(R.string.tab_tray_add_new_collection) + .setView(layout).setPositiveButton(android.R.string.ok) { dialog, _ -> + val sessionList = onPositiveButtonClick.invoke(null) + + MainScope().launch { + storage.createCollection( + collectionNameEditText.text.toString(), + sessionList + ) + } + + dialog.dismiss() + } + .setNegativeButton(android.R.string.cancel) { dialog, _ -> + onNegativeButtonClick.invoke() + dialog.cancel() + } + .create() + .show() + + collectionNameEditText.setSelection(0, collectionNameEditText.text.length) + collectionNameEditText.showKeyboard() +} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionsListAdapter.kt similarity index 90% rename from app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt rename to app/src/main/java/org/mozilla/fenix/collections/CollectionsListAdapter.kt index 41fba7213..71ede56df 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/CollectionsAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionsListAdapter.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix.tabtray +package org.mozilla.fenix.collections import android.view.LayoutInflater import android.view.ViewGroup @@ -14,10 +14,14 @@ import androidx.recyclerview.widget.RecyclerView import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import org.mozilla.fenix.R -internal class CollectionsAdapter( +/** + * An adapter for displaying an option to create a new collection and the list of existing + * collections. + */ +class CollectionsListAdapter( private val collections: Array, private val onNewCollectionClicked: () -> Unit -) : RecyclerView.Adapter() { +) : RecyclerView.Adapter() { @VisibleForTesting internal var checkedPosition = 1 diff --git a/app/src/main/java/org/mozilla/fenix/components/AbstractBinding.kt b/app/src/main/java/org/mozilla/fenix/components/AbstractBinding.kt new file mode 100644 index 000000000..cdc9fc755 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/AbstractBinding.kt @@ -0,0 +1,43 @@ +/* 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 androidx.annotation.CallSuper +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature + +/** + * Helper class for creating small binding classes that are responsible for reacting to state + * changes. + * + * Taken with ♥️ from Focus. + */ +abstract class AbstractBinding( + private val store: Store +) : LifecycleAwareFeature { + private var scope: CoroutineScope? = null + + @OptIn(ExperimentalCoroutinesApi::class) + @CallSuper + override fun start() { + scope = store.flowScoped { flow -> + onState(flow) + } + } + + @CallSuper + override fun stop() { + scope?.cancel() + } + + abstract suspend fun onState(flow: Flow) +} diff --git a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt index 109083de1..dddf03456 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -22,7 +22,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ReleaseChannel import org.mozilla.fenix.components.metrics.AdjustMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService -import org.mozilla.fenix.components.metrics.LeanplumMetricsService import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings @@ -88,13 +87,10 @@ class Analytics( ) } - val leanplumMetricsService by lazyMonitored { LeanplumMetricsService(context as Application) } - val metrics: MetricController by lazyMonitored { MetricController.create( listOf( GleanMetricsService(context, lazy { context.components.core.store }), - leanplumMetricsService, AdjustMetricsService(context as Application) ), isDataTelemetryEnabled = { context.settings().isTelemetryEnabled }, 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 086d89771..eaa06cedf 100644 --- a/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt +++ b/app/src/main/java/org/mozilla/fenix/components/BackgroundServices.kt @@ -216,14 +216,9 @@ internal class TelemetryAccountObserver( }?.let { metricController.track(it) } - - // Used by Leanplum as a context variable. - settings.fxaSignedIn = true } override fun onLoggedOut() { metricController.track(Event.SyncAuthSignOut) - // Used by Leanplum as a context variable. - settings.fxaSignedIn = false } } 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 6522fed85..a9e973a3c 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -24,10 +24,13 @@ import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.autofill.AutofillUnlockActivity -import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.components.metrics.AppStartupTelemetry import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.perf.AppStartReasonProvider +import org.mozilla.fenix.perf.StartupActivityLog +import org.mozilla.fenix.perf.StartupStateProvider +import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.Mockable @@ -68,7 +71,8 @@ class Components(private val context: Context) { core.sessionManager, core.store, core.webAppShortcutManager, - core.topSitesStorage + core.topSitesStorage, + core.bookmarksStorage ) } @@ -173,4 +177,8 @@ class Components(private val context: Context) { httpClient = core.client ) } + + val appStartReasonProvider by lazyMonitored { AppStartReasonProvider() } + val startupActivityLog by lazyMonitored { StartupActivityLog() } + val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) } } 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 a560f1288..837d00ee4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -35,6 +35,7 @@ import mozilla.components.feature.downloads.DownloadMiddleware import mozilla.components.feature.logins.exceptions.LoginExceptionStorage import mozilla.components.feature.media.MediaSessionFeature import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware +import mozilla.components.feature.prompts.PromptMiddleware import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.readerview.ReaderViewMiddleware @@ -64,7 +65,6 @@ import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.TelemetryMiddleware import org.mozilla.fenix.components.search.SearchMigration import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.components @@ -76,6 +76,7 @@ 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.telemetry.TelemetryMiddleware import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.getUndoDelay @@ -194,7 +195,8 @@ class Core( additionalBundledSearchEngineIds = listOf("reddit", "youtube"), migration = SearchMigration(context) ), - RecordingDevicesMiddleware(context) + RecordingDevicesMiddleware(context), + PromptMiddleware() ) BrowserStore( @@ -335,6 +337,13 @@ class Core( SupportUtils.JD_URL ) ) + + defaultTopSites.add( + Pair( + context.getString(R.string.default_top_site_pdd), + SupportUtils.PDD_URL + ) + ) } else { defaultTopSites.add( Pair( diff --git a/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt index 1f03bb8d4..642e65a41 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FindInPageIntegration.kt @@ -5,7 +5,10 @@ package org.mozilla.fenix.components import android.view.View +import android.view.ViewGroup.MarginLayoutParams import android.view.ViewStub +import androidx.annotation.VisibleForTesting +import androidx.core.view.isVisible import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.toolbar.BrowserToolbar @@ -13,30 +16,100 @@ import mozilla.components.concept.engine.EngineView import mozilla.components.feature.findinpage.FindInPageFeature import mozilla.components.feature.findinpage.view.FindInPageView import mozilla.components.support.base.feature.LifecycleAwareFeature +import org.mozilla.fenix.components.FindInPageIntegration.ToolbarInfo import org.mozilla.fenix.utils.Mockable +/** + * BrowserFragment delegate to handle all layout updates needed to show or hide the find in page bar. + * + * @param store [BrowserStore] + * @param sessionId ID of the [store] session in which the query will be performed. + * @param engineView the browser in which the queries will be made and which needs to be better positioned + * to suit the find in page bar. + * @param toolbarInfo [ToolbarInfo] used to configure the [BrowserToolbar] while the find in page bar is shown. + */ @Mockable class FindInPageIntegration( private val store: BrowserStore, private val sessionId: String? = null, stub: ViewStub, private val engineView: EngineView, - private val toolbar: BrowserToolbar + private val toolbarInfo: ToolbarInfo ) : InflationAwareFeature(stub) { override fun onViewInflated(view: View): LifecycleAwareFeature { return FindInPageFeature(store, view as FindInPageView, engineView) { - toolbar.visibility = View.VISIBLE + restorePreviousLayout() + view.visibility = View.GONE } } override fun onLaunch(view: View, feature: LifecycleAwareFeature) { store.state.findCustomTabOrSelectedTab(sessionId)?.let { tab -> - // Always hide the toolbar and display find in page query - toolbar.visibility = View.GONE + prepareLayoutForFindBar() + view.visibility = View.VISIBLE (feature as FindInPageFeature).bind(tab) - view.layoutParams.height = toolbar.height + view.layoutParams.height = toolbarInfo.toolbar.height } } + + @VisibleForTesting + internal fun restorePreviousLayout() { + toolbarInfo.toolbar.isVisible = true + + val engineViewParent = getEngineViewParent() + val engineViewParentParams = getEngineViewsParentLayoutParams() + if (toolbarInfo.isToolbarPlacedAtTop) { + if (toolbarInfo.isToolbarDynamic) { + engineViewParent.translationY = toolbarInfo.toolbar.height.toFloat() + engineViewParentParams.bottomMargin = 0 + } else { + engineViewParent.translationY = 0f + } + } else { + if (toolbarInfo.isToolbarDynamic) { + engineViewParentParams.bottomMargin = 0 + } + } + } + + @VisibleForTesting + internal fun prepareLayoutForFindBar() { + toolbarInfo.toolbar.isVisible = false + + val engineViewParent = getEngineViewParent() + val engineViewParentParams = getEngineViewsParentLayoutParams() + if (toolbarInfo.isToolbarPlacedAtTop) { + if (toolbarInfo.isToolbarDynamic) { + // With a dynamic toolbar the EngineView extends to the entire (top and bottom) of the screen. + // And now with the toolbar expanded it is translated down immediately below the toolbar. + engineViewParent.translationY = 0f + engineViewParentParams.bottomMargin = toolbarInfo.toolbar.height + } else { + // With a fixed toolbar the EngineView is anchored below the toolbar with 0 Y translation. + engineViewParent.translationY = -toolbarInfo.toolbar.height.toFloat() + } + } else { + // With a bottom toolbar the EngineView is already anchored to the top of the screen. + // Need just to ensure space for the find in page bar under the engineView. + engineViewParentParams.bottomMargin = toolbarInfo.toolbar.height + } + } + + @VisibleForTesting + internal fun getEngineViewParent() = engineView.asView().parent as View + + @VisibleForTesting + internal fun getEngineViewsParentLayoutParams() = getEngineViewParent().layoutParams as MarginLayoutParams + + /** + * Holder of all details needed about the Toolbar. + * Used to modify the layout of BrowserToolbar while the find in page bar is shown. + */ + data class ToolbarInfo( + val toolbar: BrowserToolbar, + val isToolbarDynamic: Boolean, + val isToolbarPlacedAtTop: Boolean + ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt b/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt index c863f748e..b6f66cc7b 100644 --- a/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt +++ b/app/src/main/java/org/mozilla/fenix/components/FxaServer.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.components import android.content.Context import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.ServerConfig.Server +import org.mozilla.fenix.Config import org.mozilla.fenix.ext.settings /** @@ -17,10 +18,18 @@ object FxaServer { const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" fun config(context: Context): ServerConfig { + // If a server override is configured, use that. Otherwise: + // - for all channels other than Mozilla Online, use Server.Release. + // - for Mozilla Online channel, if domestic server is allowed, use Server.CHINA; otherwise, use Server.RELEASE val serverOverride = context.settings().overrideFxAServer val tokenServerOverride = context.settings().overrideSyncTokenServer.ifEmpty { null } if (serverOverride.isEmpty()) { - return ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL, tokenServerOverride) + val releaseServer = if (Config.channel.isMozillaOnline && context.settings().allowDomesticChinaFxaServer) { + Server.CHINA + } else { + Server.RELEASE + } + return ServerConfig(releaseServer, CLIENT_ID, REDIRECT_URL, tokenServerOverride) } return ServerConfig(serverOverride, CLIENT_ID, REDIRECT_URL, tokenServerOverride) } 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 8f0a677c4..3bbb338b9 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PerformanceComponent.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.components import mozilla.components.support.utils.RunWhenReadyQueue +import org.mozilla.fenix.perf.ColdStartupDurationTelemetry import org.mozilla.fenix.perf.VisualCompletenessQueue import org.mozilla.fenix.perf.lazyMonitored @@ -13,4 +14,5 @@ import org.mozilla.fenix.perf.lazyMonitored */ class PerformanceComponent { val visualCompletenessQueue by lazyMonitored { VisualCompletenessQueue(RunWhenReadyQueue()) } + val coldStartupDurationTelemetry by lazyMonitored { ColdStartupDurationTelemetry() } } 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 9249c8ab8..bd9a4fb21 100644 --- a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -8,6 +8,7 @@ import android.content.Context import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.Engine +import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.contextmenu.ContextMenuUseCases import mozilla.components.feature.downloads.DownloadsUseCases @@ -23,6 +24,7 @@ import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSitesStorage import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.support.locale.LocaleUseCases +import org.mozilla.fenix.components.bookmarks.BookmarksUseCase import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.utils.Mockable @@ -38,7 +40,8 @@ class UseCases( private val sessionManager: SessionManager, private val store: BrowserStore, private val shortcutManager: WebAppShortcutManager, - private val topSitesStorage: TopSitesStorage + private val topSitesStorage: TopSitesStorage, + private val bookmarksStorage: BookmarksStorage ) { /** * Use cases that provide engine interactions for a given browser session. @@ -94,4 +97,9 @@ class UseCases( * Use cases that handle locale management. */ val localeUseCases by lazyMonitored { LocaleUseCases(store) } + + /** + * Use cases that provide bookmark management. + */ + val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage) } } diff --git a/app/src/main/java/org/mozilla/fenix/components/accounts/FenixAccountManager.kt b/app/src/main/java/org/mozilla/fenix/components/accounts/FenixAccountManager.kt new file mode 100644 index 000000000..fe5397bd1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/accounts/FenixAccountManager.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.components.accounts + +import android.content.Context +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.ext.components + +/** + * Component which holds a reference to [FxaAccountManager]. Manages account authentication, + * profiles, and profile state observers. + */ +open class FenixAccountManager(context: Context) { + val accountManager = context.components.backgroundServices.accountManager + + val authenticatedAccount + get() = accountManager.authenticatedAccount() != null + + val accountProfileEmail + get() = accountManager.accountProfile()?.email + + /** + * Check if the current account is signed in and authenticated. + */ + fun signedInToFxa(): Boolean { + val account = accountManager.authenticatedAccount() + val needsReauth = accountManager.accountNeedsReauth() + + return account != null && !needsReauth + } +} diff --git a/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt b/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt new file mode 100644 index 000000000..e381312fa --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/bookmarks/BookmarksUseCase.kt @@ -0,0 +1,42 @@ +/* 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.bookmarks + +import androidx.annotation.WorkerThread +import mozilla.appservices.places.BookmarkRoot +import mozilla.components.concept.storage.BookmarksStorage + +/** + * Use cases that allow for modifying bookmarks. + */ +class BookmarksUseCase(storage: BookmarksStorage) { + + class AddBookmarksUseCase internal constructor(private val storage: BookmarksStorage) { + + /** + * Adds a new bookmark with the provided [url] and [title]. + * + * @return The result if the operation was executed or not. A bookmark may not be added if + * one with the identical [url] already exists. + */ + @WorkerThread + suspend operator fun invoke(url: String, title: String, position: Int? = null): Boolean { + val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == it.url } == null + + if (canAdd) { + storage.addItem( + BookmarkRoot.Mobile.id, + url = url, + title = title, + position = position + ) + } + + return canAdd + } + } + + val addBookmark by lazy { AddBookmarksUseCase(storage) } +} 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 index dc3411304..333fda67f 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/AppStartupTelemetry.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/AppStartupTelemetry.kt @@ -33,6 +33,12 @@ import java.lang.reflect.Modifier.PRIVATE * Sample = [source = COLD, type = APP_ICON, hasSavedInstanceState = false,launchTimeNanoSeconds = 1824000000] * The basic idea is to collect these metrics from different phases of startup through * [AppAllStartup] and finally report them on Activity's onResume() function. + * + * **THIS CLASS HAS A KNOWN FLAW:** for COLD start, it doesn't take into account if the process is + * already running when the app starts, possibly inflating results (e.g. a Service started the + * process 20min ago and only now is HomeActivity launching). Future telemetry implementations should + * probably move in the ideological direction of [org.mozilla.fenix.perf.ColdStartupDurationTelemetry]: + * simplicity rather than comprehensiveness. */ @Suppress("TooManyFunctions") class AppStartupTelemetry( 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 c6d17dfed..aff2f88d8 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 @@ -182,6 +182,7 @@ sealed class Event { object ClosedExistingTab : Event() object TabsTrayPrivateModeTapped : Event() object TabsTrayNormalModeTapped : Event() + object TabsTraySyncedModeTapped : Event() object NewTabTapped : Event() object NewPrivateTabTapped : Event() object TabsTrayMenuOpened : Event() @@ -224,6 +225,17 @@ sealed class Event { object SearchSuggestionClicked : Event() object OpenedTabSuggestionClicked : Event() + // Set default browser experiment metrics + object SetDefaultBrowserNewTabClicked : Event() + object CloseExperimentCardClicked : Event() + object ToolbarMenuShown : Event() + object SetDefaultBrowserToolbarMenuClicked : Event() + object SetDefaultBrowserSettingsScreenClicked : Event() + + // Home menu interaction + object HomeMenuSettingsItemClicked : Event() + object HomeScreenDisplayed : Event() + // Interaction events with extras data class TopSiteSwipeCarousel(val page: Int) : Event() { @@ -325,6 +337,11 @@ sealed class Event { get() = hashMapOf(Addons.openAddonInToolbarMenuKeys.addonId to addonId) } + data class AddonOpenSetting(val addonId: String) : Event() { + override val extras: Map? + get() = hashMapOf(Addons.openAddonSettingKeys.addonId to addonId) + } + data class TipDisplayed(val identifier: String) : Event() { override val extras: Map? get() = hashMapOf(Tip.displayedKeys.identifier to identifier) @@ -577,7 +594,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, DOWNLOADS + BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS, SET_DEFAULT_BROWSER, SYNC_ACCOUNT } override val extras: Map? @@ -597,7 +614,7 @@ sealed class Event { data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() { enum class AutoplaySetting { - BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL + BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL, ALLOW_ALL } 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 6ae3601b3..7eb8ee48b 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 @@ -32,8 +32,11 @@ import org.mozilla.fenix.GleanMetrics.DownloadsMisc import org.mozilla.fenix.GleanMetrics.DownloadsManagement import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.Events +import org.mozilla.fenix.GleanMetrics.ExperimentsDefaultBrowser import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.History +import org.mozilla.fenix.GleanMetrics.HomeMenu +import org.mozilla.fenix.GleanMetrics.HomeScreen import org.mozilla.fenix.GleanMetrics.LoginDialog import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.MasterPassword @@ -52,6 +55,8 @@ import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.SearchSuggestions import org.mozilla.fenix.GleanMetrics.SearchWidget +import org.mozilla.fenix.GleanMetrics.SetDefaultNewtabExperiment +import org.mozilla.fenix.GleanMetrics.SetDefaultSettingExperiment import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.SyncedTabs @@ -192,6 +197,15 @@ private val Event.wrapper: EventWrapper<*>? { Events.browserMenuAction.record(it) }, { Events.browserMenuActionKeys.valueOf(it) } ) + is Event.SetDefaultBrowserToolbarMenuClicked -> EventWrapper( + { ExperimentsDefaultBrowser.toolbarMenuClicked.record(it) } + ) + is Event.ToolbarMenuShown -> EventWrapper( + { Events.toolbarMenuVisible.record(it) } + ) + is Event.ChangedToDefaultBrowser -> EventWrapper( + { Events.defaultBrowserChanged.record(it) } + ) is Event.OpenedBookmark -> EventWrapper( { BookmarksManagement.open.record(it) } ) @@ -586,6 +600,10 @@ private val Event.wrapper: EventWrapper<*>? { Addons.openAddonInToolbarMenu.record(it) }, { Addons.openAddonInToolbarMenuKeys.valueOf(it) } ) + is Event.AddonOpenSetting -> EventWrapper( + { Addons.openAddonSetting.record(it) }, + { Addons.openAddonSettingKeys.valueOf(it) } + ) is Event.TipDisplayed -> EventWrapper( { Tip.displayed.record(it) }, { Tip.displayedKeys.valueOf(it) } @@ -667,6 +685,9 @@ private val Event.wrapper: EventWrapper<*>? is Event.TabsTrayNormalModeTapped -> EventWrapper( { TabsTray.normalModeTapped.record(it) } ) + is Event.TabsTraySyncedModeTapped -> EventWrapper( + { TabsTray.syncedModeTapped.record(it) } + ) is Event.NewTabTapped -> EventWrapper( { TabsTray.newTabTapped.record(it) } ) @@ -805,6 +826,22 @@ private val Event.wrapper: EventWrapper<*>? is Event.SecurePrefsReset -> EventWrapper( { AndroidKeystoreExperiment.reset.record(it) } ) + is Event.HomeMenuSettingsItemClicked -> EventWrapper( + { HomeMenu.settingsItemClicked.record(it) } + ) + + is Event.CloseExperimentCardClicked -> EventWrapper( + { SetDefaultNewtabExperiment.closeExperimentCardClicked.record(it) } + ) + is Event.SetDefaultBrowserNewTabClicked -> EventWrapper( + { SetDefaultNewtabExperiment.setDefaultBrowserClicked.record(it) } + ) + is Event.SetDefaultBrowserSettingsScreenClicked -> EventWrapper( + { SetDefaultSettingExperiment.setDefaultBrowserClicked.record(it) } + ) + is Event.HomeScreenDisplayed -> EventWrapper( + { HomeScreen.homeScreenDisplayed.record(it) } + ) // Don't record other events in Glean: is Event.AddBookmark -> null @@ -815,7 +852,6 @@ private val Event.wrapper: EventWrapper<*>? is Event.FennecToFenixMigrated -> null is Event.AddonInstalled -> null is Event.SearchWidgetInstalled -> null - is Event.ChangedToDefaultBrowser -> null is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> null } 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 deleted file mode 100644 index 79526cd92..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/LeanplumMetricsService.kt +++ /dev/null @@ -1,247 +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.app.Application -import android.net.Uri -import android.util.Log -import com.leanplum.Leanplum -import com.leanplum.LeanplumActivityHelper -import com.leanplum.annotations.Parser -import com.leanplum.internal.LeanplumInternal -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -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.settings -import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor -import java.util.Locale -import java.util.MissingResourceException -import java.util.UUID.randomUUID - -private val Event.name: String? - get() = when (this) { - is Event.AddBookmark -> "E_Add_Bookmark" - is Event.RemoveBookmark -> "E_Remove_Bookmark" - is Event.OpenedBookmark -> "E_Opened_Bookmark" - is Event.OpenedApp -> "E_Opened_App" - is Event.OpenedAppFirstRun -> "E_Opened_App_FirstRun" - is Event.InteractWithSearchURLArea -> "E_Interact_With_Search_URL_Area" - is Event.CollectionSaved -> "E_Collection_Created" - is Event.CollectionTabRestored -> "E_Collection_Tab_Opened" - is Event.SyncAuthSignUp -> "E_FxA_New_Signup" - is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA" - is Event.SyncAuthFromSharedCopy, Event.SyncAuthFromSharedReuse -> "E_Sign_In_FxA_Fennec_to_Fenix" - is Event.SyncAuthSignOut -> "E_Sign_Out_FxA" - is Event.ClearedPrivateData -> "E_Cleared_Private_Data" - is Event.DismissedOnboarding -> "E_Dismissed_Onboarding" - is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated" - is Event.AddonInstalled -> "E_Addon_Installed" - is Event.SearchWidgetInstalled -> "E_Search_Widget_Added" - is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix" - is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP" - - // Do not track other events in Leanplum - else -> null - } - -class LeanplumMetricsService( - private val application: Application -) : MetricsService, DeepLinkIntentProcessor.DeepLinkVerifier { - val scope = CoroutineScope(Dispatchers.IO) - var leanplumJob: Job? = null - - data class Token(val id: String, val token: String) { - enum class Type { Development, Production, Invalid } - - val type by lazy { - when { - token.take(ProdPrefix.length) == ProdPrefix -> Type.Production - token.take(DevPrefix.length) == DevPrefix -> Type.Development - else -> Type.Invalid - } - } - - companion object { - private const val ProdPrefix = "prod" - private const val DevPrefix = "dev" - } - } - - override val type = MetricServiceType.Marketing - private val token = Token(LeanplumId, LeanplumToken) - - @Suppress("ComplexMethod") - override fun start() { - - if (!application.settings().isMarketingTelemetryEnabled) return - - Leanplum.setIsTestModeEnabled(false) - Leanplum.setApplicationContext(application) - Leanplum.setDeviceId(randomUUID().toString()) - Parser.parseVariables(application) - - leanplumJob = scope.launch { - - val applicationSetLocale = LocaleManager.getCurrentLocale(application) - val currentLocale = applicationSetLocale ?: Locale.getDefault() - val languageCode = - currentLocale.iso3LanguageOrNull - ?: currentLocale.language.let { - if (it.isNotBlank()) { - it - } else { - currentLocale.toString() - } - } - - if (!isLeanplumEnabled(languageCode)) { - Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode") - return@launch - } - - when (token.type) { - Token.Type.Production -> Leanplum.setAppIdForProductionMode(token.id, token.token) - Token.Type.Development -> Leanplum.setAppIdForDevelopmentMode(token.id, token.token) - Token.Type.Invalid -> { - Log.i(LOGTAG, "Invalid or missing Leanplum token") - return@launch - } - } - - LeanplumActivityHelper.enableLifecycleCallbacks(application) - - val installedApps = MozillaProductDetector.getInstalledMozillaProducts(application) - - val trackingProtection = application.settings().run { - when { - !shouldUseTrackingProtection -> "none" - useStandardTrackingProtection -> "standard" - useStrictTrackingProtection -> "strict" - else -> "custom" - } - } - - Leanplum.start( - application, hashMapOf( - "default_browser" to MozillaProductDetector.getMozillaBrowserDefault(application) - .orEmpty(), - "fennec_installed" to installedApps.contains(MozillaProducts.FIREFOX.productName), - "focus_installed" to installedApps.contains(MozillaProducts.FOCUS.productName), - "klar_installed" to installedApps.contains(MozillaProducts.KLAR.productName), - "fxa_signed_in" to application.settings().fxaSignedIn, - "fxa_has_synced_items" to application.settings().fxaHasSyncedItems, - "search_widget_installed" to application.settings().searchWidgetInstalled, - "tracking_protection_enabled" to application.settings().shouldUseTrackingProtection, - "tracking_protection_setting" to trackingProtection, - "fenix" to true - ) - ) - - withContext(Main) { - LeanplumInternal.setCalledStart(true) - LeanplumInternal.setHasStarted(true) - LeanplumInternal.setStartedInBackground(true) - Log.i(LOGTAG, "Started Leanplum with deviceId ${Leanplum.getDeviceId()}" + - " and userId ${Leanplum.getUserId()}") - } - } - } - - /** - * Verifies a deep link and returns `true` for deep links that should be handled and `false` if - * a deep link should be rejected. - * - * @See DeepLinkIntentProcessor.verifier - */ - override fun verifyDeepLink(deepLink: Uri): Boolean { - // We compare the local Leanplum device ID against the "uid" query parameter and only - // accept deep links where both values match. - val uid = deepLink.getQueryParameter("uid") - return uid == Leanplum.getDeviceId() - } - - override fun stop() { - if (application.settings().isMarketingTelemetryEnabled) return - // As written in LeanPlum SDK documentation, "This prevents Leanplum from communicating with the server." - // as this "isTestMode" flag is checked before LeanPlum SDK does anything. - // Also has the benefit effect of blocking the display of already downloaded messages. - // The reverse of this - setIsTestModeEnabled(false) must be called before trying to init - // LP in the same session. - Leanplum.setIsTestModeEnabled(true) - - // This is just to allow restarting LP and it's functionality in the same app session - // as LP stores it's state internally and check against it - LeanplumInternal.setCalledStart(false) - LeanplumInternal.setHasStarted(false) - leanplumJob?.cancel() - } - - override fun track(event: Event) { - val leanplumExtras = event.extras - ?.map { (key, value) -> key.toString() to value } - ?.toMap() - - event.name?.also { - Leanplum.track(it, leanplumExtras) - } - } - - override fun shouldTrack(event: Event): Boolean { - return application.settings().isTelemetryEnabled && - token.type != Token.Type.Invalid && !event.name.isNullOrEmpty() - } - - private fun isLeanplumEnabled(locale: String): Boolean { - return LEANPLUM_ENABLED_LOCALES.contains(locale) - } - - private val Locale.iso3LanguageOrNull: String? - get() = - try { - this.isO3Language - } catch (_: MissingResourceException) { - null - } - - companion object { - private const val LOGTAG = "LeanplumMetricsService" - - private val LeanplumId: String - // Debug builds have a null (nullable) LEANPLUM_ID - get() = BuildConfig.LEANPLUM_ID.orEmpty() - private val LeanplumToken: String - // Debug builds have a null (nullable) LEANPLUM_TOKEN - get() = BuildConfig.LEANPLUM_TOKEN.orEmpty() - - // Leanplum needs to be enabled for the following locales. - // Irrespective of the actual device location. - private val LEANPLUM_ENABLED_LOCALES = setOf( - "eng", // English - "zho", // Chinese - "deu", // German - "fra", // French - "ita", // Italian - "ind", // Indonesian - "por", // Portuguese - "spa", // Spanish; Castilian - "pol", // Polish - "rus", // Russian - "hin", // Hindi - "per", // Persian - "fas", // Persian - "ara", // Arabic - "jpn" // Japanese - ) - - private const val PREFERENCE_NAME = "LEANPLUM_PREFERENCES" - private const val DEVICE_ID_KEY = "LP_DEVICE_ID" - } -} 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 181ff4ea2..f10182b46 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,7 +5,6 @@ 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 @@ -179,7 +178,7 @@ internal class ReleaseMetricController( } Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { - metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } + metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } ?: Event.ToolbarMenuShown } Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } @@ -218,7 +217,6 @@ internal class ReleaseMetricController( if (installedAddons is List<*>) { settings.installedAddonsCount = installedAddons.size settings.installedAddonsList = installedAddons.joinToString(",") - Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size)) } } @@ -226,7 +224,6 @@ internal class ReleaseMetricController( if (enabledAddons is List<*>) { settings.enabledAddonsCount = enabledAddons.size settings.enabledAddonsList = enabledAddons.joinToString() - 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 942390abd..fa303fc23 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 @@ -24,6 +24,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.HomeScreenViewModel @@ -118,7 +119,7 @@ class DefaultBrowserToolbarController( // When closing the last tab we must show the undo snackbar in the home fragment if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) { homeViewModel.sessionToDelete = it.id - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalHome() ) } else { @@ -132,7 +133,7 @@ class DefaultBrowserToolbarController( Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB) ) activity.browsingModeManager.mode = BrowsingMode.Normal - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) ) } @@ -143,7 +144,7 @@ class DefaultBrowserToolbarController( ) ) activity.browsingModeManager.mode = BrowsingMode.Private - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt index e5bc0f71e..8f7fdd0f0 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -39,8 +39,10 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.navigateSafe +import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Settings @@ -91,7 +93,7 @@ class DefaultBrowserToolbarMenuController( Do exhaustive when (item) { // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 // todo === Start === - is ToolbarMenu.Item.InstallToHomeScreen -> { + is ToolbarMenu.Item.InstallPwaToHomeScreen -> { settings.installPwaOpened = true MainScope().launch { with(activity.components.useCases.webAppUseCases) { @@ -128,6 +130,18 @@ class DefaultBrowserToolbarMenuController( activity.finishAndRemoveTask() } } + // todo === End === + is ToolbarMenu.Item.OpenInApp -> { + settings.openInAppOpened = true + + val appLinksUseCases = activity.components.useCases.appLinksUseCases + val getRedirect = appLinksUseCases.appLinkRedirect + currentSession?.let { + val redirect = getRedirect.invoke(it.content.url) + redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK + appLinksUseCases.openAppLink.invoke(redirect.appIntent) + } + } is ToolbarMenu.Item.Quit -> { // We need to show the snackbar while the browsing data is deleting (if "Delete // browsing data on quit" is activated). After the deletion is over, the snackbar @@ -147,22 +161,9 @@ class DefaultBrowserToolbarMenuController( readerModeController.showControls() metrics.track(Event.ReaderModeAppearanceOpened) } - is ToolbarMenu.Item.OpenInApp -> { - settings.openInAppOpened = true - - val appLinksUseCases = activity.components.useCases.appLinksUseCases - val getRedirect = appLinksUseCases.appLinkRedirect - currentSession?.let { - val redirect = getRedirect.invoke(it.content.url) - redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK - appLinksUseCases.openAppLink.invoke(redirect.appIntent) - } - } - // todo === End === - is ToolbarMenu.Item.Back -> { if (item.viewHistory) { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId ) @@ -175,7 +176,7 @@ class DefaultBrowserToolbarMenuController( } is ToolbarMenu.Item.Forward -> { if (item.viewHistory) { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( activeSessionId = customTabSessionId ) @@ -212,7 +213,7 @@ class DefaultBrowserToolbarMenuController( ), showPage = true ) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() @@ -224,6 +225,19 @@ class DefaultBrowserToolbarMenuController( BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment() ) } + is ToolbarMenu.Item.SyncAccount -> { + val directions = if (item.signedIn) { + BrowserFragmentDirections.actionGlobalAccountSettingsFragment() + } else { + BrowserFragmentDirections.actionGlobalTurnOnSync() + } + browserAnimator.captureEngineViewAndDrawStatically { + navController.nav( + R.id.browserFragment, + directions + ) + } + } is ToolbarMenu.Item.RequestDesktop -> { currentSession?.let { sessionUseCases.requestDesktopSite.invoke( @@ -335,10 +349,14 @@ class DefaultBrowserToolbarMenuController( ) } is ToolbarMenu.Item.NewTab -> { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) ) } + is ToolbarMenu.Item.SetDefaultBrowser -> { + metrics.track(Event.SetDefaultBrowserToolbarMenuClicked) + activity.openSetDefaultBrowserOption() + } } } @@ -356,15 +374,12 @@ class DefaultBrowserToolbarMenuController( @Suppress("ComplexMethod") private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { val eventItem = when (item) { - // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 - // todo === Start === is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX - is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN + is ToolbarMenu.Item.InstallPwaToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT + is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP is ToolbarMenu.Item.CustomizeReaderView -> Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE - is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP - // todo === End === is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD @@ -382,12 +397,14 @@ class DefaultBrowserToolbarMenuController( is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS + is ToolbarMenu.Item.SyncAccount -> Event.BrowserMenuItemTapped.Item.SYNC_ACCOUNT is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB + is ToolbarMenu.Item.SetDefaultBrowser -> Event.BrowserMenuItemTapped.Item.SET_DEFAULT_BROWSER } metrics.track(Event.BrowserMenuItemTapped(eventItem)) diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index a5d666f21..154a9c63e 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -168,14 +168,13 @@ class BrowserToolbarView( } else { menuToolbar = DefaultToolbarMenu( context = this, + store = components.core.store, hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(), - shouldReverseItems = toolbarPosition == ToolbarPosition.TOP, onItemTapped = { it.performHapticIfNeeded(view) interactor.onBrowserToolbarMenuItemTapped(it) }, lifecycleOwner = lifecycleOwner, - store = components.core.store, bookmarksStorage = bookmarkStorage, isPinningSupported = isPinningSupported ) 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 38ac495d2..a4b4737b8 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 @@ -7,6 +7,7 @@ package org.mozilla.fenix.components.toolbar import android.content.Context import androidx.annotation.ColorRes import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE import androidx.core.content.ContextCompat.getColor import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -21,6 +22,7 @@ import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuImageSwitch import mozilla.components.browser.menu.item.BrowserMenuImageText +import mozilla.components.browser.menu.item.BrowserMenuImageTextCheckboxButton import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem import mozilla.components.browser.state.selector.findTab @@ -33,13 +35,19 @@ import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.FeatureFlags.tabsTrayRewrite import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.components.accounts.FenixAccountManager +import org.mozilla.fenix.experiments.ExperimentBranch +import org.mozilla.fenix.experiments.Experiments import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.ext.withExperiment import org.mozilla.fenix.theme.ThemeManager +import org.mozilla.fenix.utils.BrowsersCache /** * Builds the toolbar object used with the 3-dot menu in the browser fragment. @@ -50,13 +58,12 @@ import org.mozilla.fenix.theme.ThemeManager * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. * @param bookmarksStorage Used to check if a page is bookmarked. */ -@Suppress("LargeClass", "LongParameterList") +@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @ExperimentalCoroutinesApi -class DefaultToolbarMenu( +open class DefaultToolbarMenu( private val context: Context, private val store: BrowserStore, hasAccountProblem: Boolean = false, - shouldReverseItems: Boolean, private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}, private val lifecycleOwner: LifecycleOwner, private val bookmarksStorage: BookmarksStorage, @@ -65,7 +72,11 @@ class DefaultToolbarMenu( private var isCurrentUrlBookmarked = false private var isBookmarkedJob: Job? = null - private val isTopToolbarSelected = shouldReverseItems + + private val shouldDeleteDataOnQuit = context.settings().shouldDeleteBrowsingDataOnQuit + private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar + private val accountManager = FenixAccountManager(context) + private val selectedSession: TabSessionState? get() = store.state.selectedTab @@ -77,13 +88,16 @@ class DefaultToolbarMenu( } else { oldCoreMenuItems }, - endOfMenuAlwaysVisible = !shouldReverseItems, + endOfMenuAlwaysVisible = shouldUseBottomToolbar, store = store, - webExtIconTintColorResource = primaryTextColor(), + style = WebExtensionBrowserMenuBuilder.Style( + webExtIconTintColorResource = primaryTextColor(), + addonsManagerMenuItemDrawableRes = R.drawable.ic_addons_extensions + ), onAddonsManagerTapped = { onItemTapped.invoke(ToolbarMenu.Item.AddonsManager) }, - appendExtensionSubMenuAtStart = !shouldReverseItems + appendExtensionSubMenuAtStart = shouldUseBottomToolbar ) } @@ -137,7 +151,7 @@ class DefaultToolbarMenu( } val share = BrowserMenuItemToolbar.Button( - imageResource = R.drawable.ic_share_filled, + imageResource = R.drawable.ic_share, contentDescription = context.getString(R.string.browser_menu_share), iconTintColorResource = primaryTextColor(), listener = { @@ -148,7 +162,7 @@ class DefaultToolbarMenu( registerForIsBookmarkedUpdates() if (FeatureFlags.toolbarMenuFeature) { - BrowserMenuItemToolbar(listOf(back, forward, share, refresh)) + BrowserMenuItemToolbar(listOf(back, forward, share, refresh), isSticky = true) } else { val bookmark = BrowserMenuItemToolbar.TwoStateButton( primaryImageResource = R.drawable.ic_bookmark_filled, @@ -163,8 +177,7 @@ class DefaultToolbarMenu( secondaryImageTintResource = primaryTextColor(), disableInSecondaryState = false ) { - if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true - onItemTapped.invoke(ToolbarMenu.Item.Bookmark) + handleBookmarkItemTapped() } BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh)) @@ -172,24 +185,43 @@ class DefaultToolbarMenu( } // Predicates that need to be repeatedly called as the session changes - private fun canAddToHomescreen(): Boolean = + @VisibleForTesting(otherwise = PRIVATE) + fun canAddToHomescreen(): Boolean = selectedSession != null && isPinningSupported && !context.components.useCases.webAppUseCases.isInstallable() - private fun canInstall(): Boolean = + @VisibleForTesting(otherwise = PRIVATE) + fun canInstall(): Boolean = selectedSession != null && isPinningSupported && context.components.useCases.webAppUseCases.isInstallable() - private fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session -> + @VisibleForTesting(otherwise = PRIVATE) + fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session -> val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect appLink(session.content.url).hasExternalApp() } ?: false - private fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let { + @VisibleForTesting(otherwise = PRIVATE) + fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let { store.state.findTab(it.id)?.readerState?.active } ?: false // End of predicates // + val installToHomescreen = BrowserMenuHighlightableItem( + label = context.getString(R.string.browser_menu_install_on_homescreen), + startImageResource = R.drawable.ic_add_to_homescreen, + iconTintColorResource = primaryTextColor(), + highlight = BrowserMenuHighlight.LowPriority( + label = context.getString(R.string.browser_menu_install_on_homescreen), + notificationTint = getColor(context, R.color.whats_new_notification_color) + ), + isHighlighted = { + !context.settings().installPwaOpened + } + ) { + onItemTapped.invoke(ToolbarMenu.Item.InstallPwaToHomeScreen) + } + private val oldCoreMenuItems by lazy { val settings = BrowserMenuHighlightableItem( label = context.getString(R.string.browser_menu_settings), @@ -244,21 +276,6 @@ class DefaultToolbarMenu( onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) } - val installToHomescreen = BrowserMenuHighlightableItem( - label = context.getString(R.string.browser_menu_install_on_homescreen), - startImageResource = R.drawable.ic_add_to_homescreen, - iconTintColorResource = primaryTextColor(), - highlight = BrowserMenuHighlight.LowPriority( - label = context.getString(R.string.browser_menu_install_on_homescreen), - notificationTint = getColor(context, R.color.whats_new_notification_color) - ), - isHighlighted = { - !context.settings().installPwaOpened - } - ) { - onItemTapped.invoke(ToolbarMenu.Item.InstallToHomeScreen) - } - val findInPage = BrowserMenuImageText( label = context.getString(R.string.browser_menu_find_in_page), imageResource = R.drawable.mozac_ic_search, @@ -348,6 +365,9 @@ class DefaultToolbarMenu( BrowserMenuDivider(), reportSiteIssuePlaceholder, findInPage, + getSetDefaultBrowserItem()?.let { BrowserMenuDivider() }, + getSetDefaultBrowserItem(), + getSetDefaultBrowserItem()?.let { BrowserMenuDivider() }, addToTopSites, addToHomescreen.apply { visible = ::canAddToHomescreen }, installToHomescreen.apply { visible = ::canInstall }, @@ -359,157 +379,195 @@ class DefaultToolbarMenu( menuToolbar ) - if (shouldReverseItems) { - menuItems.reversed() - } else { + if (shouldUseBottomToolbar) { menuItems + } else { + menuItems.reversed() } } - private val newCoreMenuItems by lazy { - val newTabItem = BrowserMenuImageText( - context.getString(R.string.library_new_tab), - R.drawable.ic_new, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.NewTab) - } + val newTabItem = BrowserMenuImageText( + context.getString(R.string.library_new_tab), + R.drawable.ic_new, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.NewTab) + } - val bookmarksItem = BrowserMenuImageText( - context.getString(R.string.library_bookmarks), - R.drawable.ic_bookmark_filled, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) - } + val historyItem = BrowserMenuImageText( + context.getString(R.string.library_history), + R.drawable.ic_history, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.History) + } - val historyItem = BrowserMenuImageText( - context.getString(R.string.library_history), - R.drawable.ic_history, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.History) - } + val downloadsItem = BrowserMenuImageText( + context.getString(R.string.library_downloads), + R.drawable.ic_download, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Downloads) + } - val downloadsItem = BrowserMenuImageText( - context.getString(R.string.library_downloads), - R.drawable.ic_download, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.Downloads) - } + val extensionsItem = WebExtensionPlaceholderMenuItem( + id = WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID + ) - val extensionsItem = BrowserMenuImageText( - context.getString(R.string.browser_menu_extensions), - R.drawable.ic_addons_extensions, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.AddonsManager) - } + val findInPageItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_find_in_page), + imageResource = R.drawable.mozac_ic_search, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.FindInPage) + } - val syncedTabs = BrowserMenuImageText( - label = context.getString(R.string.synced_tabs), - imageResource = R.drawable.ic_synced_tabs, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) + val desktopSiteItem = BrowserMenuImageSwitch( + imageResource = R.drawable.ic_desktop, + label = context.getString(R.string.browser_menu_desktop_site), + initialState = { + selectedSession?.content?.desktopMode ?: false } + ) { checked -> + onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) + } - val findInPageItem = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_find_in_page), - imageResource = R.drawable.mozac_ic_search, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.FindInPage) - } + val customizeReaderView = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_customize_reader_view), + imageResource = R.drawable.ic_readermode_appearance, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView) + } - val desktopSiteItem = BrowserMenuImageSwitch( - imageResource = R.drawable.ic_desktop, - label = context.getString(R.string.browser_menu_desktop_site), - initialState = { - selectedSession?.content?.desktopMode ?: false - } - ) { checked -> - onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) - } + val openInApp = BrowserMenuHighlightableItem( + label = context.getString(R.string.browser_menu_open_app_link), + startImageResource = R.drawable.ic_open_in_app, + iconTintColorResource = primaryTextColor(), + highlight = BrowserMenuHighlight.LowPriority( + label = context.getString(R.string.browser_menu_open_app_link), + notificationTint = getColor(context, R.color.whats_new_notification_color) + ), + isHighlighted = { !context.settings().openInAppOpened } + ) { + onItemTapped.invoke(ToolbarMenu.Item.OpenInApp) + } - val customizeReaderView = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_customize_reader_view), - imageResource = R.drawable.ic_readermode_appearance, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView) - } + val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( + id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID + ) + + val addToHomeScreenItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_add_to_homescreen), + imageResource = R.drawable.ic_add_to_homescreen, + iconTintColorResource = primaryTextColor(), + isCollapsingMenuLimit = true + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) + } - val openInApp = BrowserMenuHighlightableItem( - label = context.getString(R.string.browser_menu_open_app_link), - startImageResource = R.drawable.ic_open_in_app, - iconTintColorResource = primaryTextColor(), - highlight = BrowserMenuHighlight.LowPriority( - label = context.getString(R.string.browser_menu_open_app_link), - notificationTint = getColor(context, R.color.whats_new_notification_color) - ), - isHighlighted = { !context.settings().openInAppOpened } - ) { - onItemTapped.invoke(ToolbarMenu.Item.OpenInApp) - } + val addToTopSitesItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_add_to_top_sites), + imageResource = R.drawable.ic_top_sites, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites) + } - val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( - id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID - ) + val saveToCollectionItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_save_to_collection_2), + imageResource = R.drawable.ic_tab_collection, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) + } - val addToHomeScreenItem = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_add_to_homescreen), - imageResource = R.drawable.ic_add_to_homescreen, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) - } + val settingsItem = BrowserMenuHighlightableItem( + label = context.getString(R.string.browser_menu_settings), + startImageResource = R.drawable.ic_settings, + iconTintColorResource = if (hasAccountProblem) + ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else + primaryTextColor(), + textColorResource = if (hasAccountProblem) + ThemeManager.resolveAttribute(R.attr.primaryText, context) else + primaryTextColor(), + highlight = BrowserMenuHighlight.HighPriority( + endImageResource = R.drawable.ic_sync_disconnected, + backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground), + canPropagate = false + ), + isHighlighted = { hasAccountProblem } + ) { + onItemTapped.invoke(ToolbarMenu.Item.Settings) + } - val addToTopSitesItem = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_add_to_top_sites), - imageResource = R.drawable.ic_top_sites, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites) - } + val bookmarksItem = BrowserMenuImageTextCheckboxButton( + imageResource = R.drawable.ic_bookmarks_menu, + iconTintColorResource = primaryTextColor(), + label = context.getString(R.string.library_bookmarks), + labelListener = { + onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) + }, + primaryStateIconResource = R.drawable.ic_bookmark_outline, + secondaryStateIconResource = R.drawable.ic_bookmark_filled, + tintColorResource = menuItemButtonTintColor(), + primaryLabel = context.getString(R.string.browser_menu_add), + secondaryLabel = context.getString(R.string.browser_menu_edit), + isInPrimaryState = { !isCurrentUrlBookmarked } + ) { + handleBookmarkItemTapped() + } - val saveToCollectionItem = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_save_to_collection_2), - imageResource = R.drawable.ic_tab_collection, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) - } + val deleteDataOnQuit = BrowserMenuImageText( + label = context.getString(R.string.delete_browsing_data_on_quit_action), + imageResource = R.drawable.ic_exit, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Quit) + } - val settingsItem = BrowserMenuHighlightableItem( - label = context.getString(R.string.browser_menu_settings), - startImageResource = R.drawable.ic_settings, - iconTintColorResource = primaryTextColor(), - textColorResource = if (hasAccountProblem) - ThemeManager.resolveAttribute(R.attr.primaryText, context) else - primaryTextColor(), - highlight = BrowserMenuHighlight.HighPriority( - endImageResource = R.drawable.ic_sync_disconnected, - backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground), - canPropagate = false - ), - isHighlighted = { hasAccountProblem } - ) { - onItemTapped.invoke(ToolbarMenu.Item.Settings) + val syncedTabsItem = BrowserMenuImageText( + context.getString(R.string.synced_tabs), + R.drawable.ic_synced_tabs, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) + } + + private fun getSyncItemTitle(): String { + val authenticatedAccount = accountManager.authenticatedAccount + val email = accountManager.accountProfileEmail + + return if (authenticatedAccount && !email.isNullOrEmpty()) { + email + } else { + context.getString(R.string.sync_menu_sign_in) } + } + + val syncMenuItem = BrowserMenuImageText( + getSyncItemTitle(), + R.drawable.ic_signed_out, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SyncAccount(accountManager.signedInToFxa())) + } + @VisibleForTesting(otherwise = PRIVATE) + val newCoreMenuItems by lazy { val menuItems = listOfNotNull( - if (isTopToolbarSelected) menuToolbar else null, + if (shouldUseBottomToolbar) null else menuToolbar, newTabItem, BrowserMenuDivider(), bookmarksItem, historyItem, downloadsItem, extensionsItem, - syncedTabs, + if (tabsTrayRewrite) syncMenuItem else syncedTabsItem, BrowserMenuDivider(), + getSetDefaultBrowserItem(), + getSetDefaultBrowserItem()?.let { BrowserMenuDivider() }, findInPageItem, desktopSiteItem, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, @@ -517,21 +575,32 @@ class DefaultToolbarMenu( reportSiteIssuePlaceholder, BrowserMenuDivider(), addToHomeScreenItem.apply { visible = ::canAddToHomescreen }, + installToHomescreen.apply { visible = ::canInstall }, addToTopSitesItem, saveToCollectionItem, BrowserMenuDivider(), settingsItem, - if (isTopToolbarSelected) null else BrowserMenuDivider(), - if (isTopToolbarSelected) null else menuToolbar + if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, + if (shouldUseBottomToolbar) BrowserMenuDivider() else null, + if (shouldUseBottomToolbar) menuToolbar else null ) menuItems } + private fun handleBookmarkItemTapped() { + if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true + onItemTapped.invoke(ToolbarMenu.Item.Bookmark) + } + @ColorRes @VisibleForTesting internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) + @ColorRes + @VisibleForTesting + internal fun menuItemButtonTintColor() = ThemeManager.resolveAttribute(R.attr.menuItemButtonTintColor, context) + @VisibleForTesting internal fun registerForIsBookmarkedUpdates() { store.flowScoped(lifecycleOwner) { flow -> @@ -558,4 +627,24 @@ class DefaultToolbarMenu( .any { it.url == newUrl } } } + + private fun getSetDefaultBrowserItem(): BrowserMenuImageText? { + val experiments = context.components.analytics.experiments + val browsers = BrowsersCache.all(context) + + return experiments.withExperiment(Experiments.DEFAULT_BROWSER) { experimentBranch -> + if (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_TOOLBAR_MENU && + !browsers.isFirefoxDefaultBrowser + ) { + return@withExperiment BrowserMenuImageText( + label = context.getString(R.string.preferences_set_as_default_browser), + imageResource = R.mipmap.ic_launcher + ) { + onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser) + } + } else { + null + } + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt index 126a949e1..ec384c188 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarMenu.kt @@ -20,12 +20,14 @@ interface ToolbarMenu { object OpenInFenix : Item() object SaveToCollection : Item() object AddToTopSites : Item() - object InstallToHomeScreen : Item() + object InstallPwaToHomeScreen : Item() object AddToHomeScreen : Item() object SyncedTabs : Item() + data class SyncAccount(val signedIn: Boolean) : Item() object AddonsManager : Item() object Quit : Item() object OpenInApp : Item() + object SetDefaultBrowser : Item() object Bookmark : Item() object CustomizeReaderView : Item() object Bookmarks : Item() 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 689a6aa4c..a7546b378 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -60,6 +60,9 @@ open class ExternalAppBrowserActivity : HomeActivity() { homeActivityInitTimeStampNanoSeconds, rootContainer ) + + // coldStartupDurationTelemetry.onHomeActivityOnCreate is intentionally omitted so we don't + // include even more unpredictable code paths in the results. } override fun navigateToBrowserOnColdStart() { diff --git a/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehavior.kt b/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehavior.kt index a5d640bae..6fc09d277 100644 --- a/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehavior.kt +++ b/app/src/main/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehavior.kt @@ -50,7 +50,8 @@ class DynamicDownloadDialogBehavior( /** * Reference to [EngineView] used to check user's [android.view.MotionEvent]s. */ - private var engineView: EngineView? = null + @VisibleForTesting + internal var engineView: EngineView? = null /** * Depending on how user's touch was consumed by EngineView / current website, @@ -64,7 +65,9 @@ class DynamicDownloadDialogBehavior( */ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal val shouldScroll: Boolean - get() = engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_HANDLED + get() = engineView?.getInputResultDetail()?.let { + (it.canScrollToBottom() || it.canScrollToTop()) + } ?: false override fun onStartNestedScroll( coordinatorLayout: CoordinatorLayout, @@ -78,7 +81,7 @@ class DynamicDownloadDialogBehavior( shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH snapAnimator.cancel() true - } else if (engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_UNHANDLED) { + } else if (engineView?.getInputResultDetail()?.isTouchUnhandled() == true) { // Force expand the notification dialog if event is unhandled, otherwise user could get stuck in a // state where they cannot show it forceExpand(child) diff --git a/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt b/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt index 9a9a39036..92808c5ff 100644 --- a/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt +++ b/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt @@ -7,8 +7,8 @@ package org.mozilla.fenix.experiments class Experiments { companion object { const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation-v3" - const val BOOKMARK_ICON = "fenix-bookmark-list-icon" const val ANDROID_KEYSTORE = "fenix-android-keystore" + const val DEFAULT_BROWSER = "fenix-default-browser" } } @@ -18,5 +18,8 @@ class ExperimentBranch { const val CONTROL = "control" const val A1 = "a1" const val A2 = "a2" + const val DEFAULT_BROWSER_TOOLBAR_MENU = "default_browser_toolbar_menu" + const val DEFAULT_BROWSER_NEW_TAB_BANNER = "default_browser_newtab_banner" + const val DEFAULT_BROWSER_SETTINGS_MENU = "default_browser_settings_menu" } } 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 db5feca2b..6469a4c88 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Activity.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Activity.kt @@ -8,6 +8,14 @@ import android.app.Activity import android.view.View import android.view.WindowManager import mozilla.components.concept.base.crash.Breadcrumb +import android.app.role.RoleManager +import android.content.Intent +import android.os.Build +import android.provider.Settings +import androidx.core.os.bundleOf +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.settings.SupportUtils /** * Attempts to call immersive mode using the View to hide the status bar and navigation buttons. @@ -15,8 +23,17 @@ import mozilla.components.concept.base.crash.Breadcrumb * We don't use the equivalent function from Android Components because the stable flag messes * with the toolbar. See #1998 and #3272. */ +@Deprecated( + message = "Use the Android Component implementation instead.", + replaceWith = ReplaceWith( + "enterToImmersiveMode()", + "mozilla.components.support.ktx.android.view.enterToImmersiveMode" + ) +) fun Activity.enterToImmersiveMode() { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + // This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17804 + @Suppress("DEPRECATION") window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION @@ -24,19 +41,6 @@ fun Activity.enterToImmersiveMode() { or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) } -/** - * Attempts to come out from immersive mode using the View. - */ -fun Activity.exitImmersiveModeIfNeeded() { - if (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON and window.attributes.flags == 0) { - // We left immersive mode already. - return - } - - window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE -} - fun Activity.breadcrumb( message: String, data: Map = emptyMap() @@ -52,3 +56,59 @@ fun Activity.breadcrumb( ) ) } + +/** + * Opens Android's Manage Default Apps Settings if possible. + */ +fun Activity.openSetDefaultBrowserOption() { + when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { + getSystemService(RoleManager::class.java).also { + if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld( + RoleManager.ROLE_BROWSER + ) + ) { + startActivityForResult( + it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), + REQUEST_CODE_BROWSER_ROLE + ) + } else { + navigateToDefaultBrowserAppsSettings() + } + } + } + Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { + navigateToDefaultBrowserAppsSettings() + } + else -> { + (this as HomeActivity).openToBrowserAndLoad( + searchTermOrURL = SupportUtils.getSumoURLForTopic( + this, + SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER + ), + newTab = true, + from = BrowserDirection.FromSettings + ) + } + } +} + +private fun Activity.navigateToDefaultBrowserAppsSettings() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) + intent.putExtra( + SETTINGS_SELECT_OPTION_KEY, + DEFAULT_BROWSER_APP_OPTION + ) + intent.putExtra( + SETTINGS_SHOW_FRAGMENT_ARGS, + bundleOf(SETTINGS_SELECT_OPTION_KEY to DEFAULT_BROWSER_APP_OPTION) + ) + startActivity(intent) + } +} + +const val REQUEST_CODE_BROWSER_ROLE = 1 +const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key" +const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args" +const val DEFAULT_BROWSER_APP_OPTION = "default_browser" diff --git a/app/src/main/java/org/mozilla/fenix/ext/NavController.kt b/app/src/main/java/org/mozilla/fenix/ext/NavController.kt index 4034c2472..8ba9cebfd 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/NavController.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/NavController.kt @@ -2,6 +2,9 @@ * 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/. */ +// We suppress the calls to `navigate` since we invoke the Android `NavController.navigate` through +// this file. Detekt checks for the `navigate()` function calls, which should be ignored in this file. +@file:Suppress("MozillaNavigateCheck") package org.mozilla.fenix.ext import androidx.annotation.IdRes @@ -10,12 +13,15 @@ import androidx.navigation.NavDirections import androidx.navigation.NavOptions import io.sentry.Sentry import org.mozilla.fenix.components.isSentryEnabled +import org.mozilla.fenix.perf.NavGraphProvider /** * Navigate from the fragment with [id] using the given [directions]. * If the id doesn't match the current destination, an error is recorded. */ fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: NavOptions? = null) { + NavGraphProvider.blockForNavGraphInflation(this) + if (id == null || this.currentDestination?.id == id) { this.navigate(directions, navOptions) } else { @@ -23,6 +29,21 @@ fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: Na } } +fun NavController.navigateBlockingForAsyncNavGraph(resId: Int) { + NavGraphProvider.blockForNavGraphInflation(this) + this.navigate(resId) +} + +fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections) { + NavGraphProvider.blockForNavGraphInflation(this) + this.navigate(directions) +} + +fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections, navOptions: NavOptions?) { + NavGraphProvider.blockForNavGraphInflation(this) + this.navigate(directions, navOptions) +} + fun NavController.alreadyOnDestination(@IdRes destId: Int?): Boolean { return destId?.let { currentDestination?.id == it || popBackStack(it, false) } ?: false } @@ -38,6 +59,6 @@ fun NavController.navigateSafe( directions: NavDirections ) { if (currentDestination?.id == resId) { - this.navigate(directions) + this.navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/String.kt b/app/src/main/java/org/mozilla/fenix/ext/String.kt index 64052c359..19c84249e 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/String.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/String.kt @@ -7,6 +7,7 @@ package org.mozilla.fenix.ext import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri +import android.text.Editable import android.util.Patterns import android.webkit.URLUtil import androidx.core.net.toUri @@ -114,6 +115,11 @@ fun String.simplifiedUrl(): String { return afterScheme } +/** + * Returns an [Editable] for the provided string. + */ +fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) + suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) { // Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once, // we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data'). 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 d909c8779..a43261f41 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -89,6 +89,7 @@ import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions +import org.mozilla.fenix.browser.BrowserFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.PrivateShortcutCreateManager @@ -102,6 +103,7 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideToolbar +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav @@ -225,7 +227,8 @@ class HomeFragment : Fragment() { ) ).getTip() }, - showCollectionPlaceholder = false + showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome, + showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard() ) ) } @@ -252,6 +255,7 @@ class HomeFragment : Fragment() { restoreUseCase = components.useCases.tabsUseCases.restore, reloadUrlUseCase = components.useCases.sessionUseCases.reload, selectTabUseCase = components.useCases.tabsUseCases.selectTab, + requestDesktopSiteUseCase = components.useCases.sessionUseCases.requestDesktopSite, fragmentStore = homeFragmentStore, navController = findNavController(), viewLifecycleScope = viewLifecycleOwner.lifecycleScope, @@ -359,82 +363,83 @@ class HomeFragment : Fragment() { @Suppress("LongMethod", "ComplexMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) = - PerfStartup.homeFragmentOnViewCreated.measureNoInline { // weird indent so we don't have to break blame. - super.onViewCreated(view, savedInstanceState) - - observeSearchEngineChanges() - createHomeMenu(requireContext(), WeakReference(view.menuButton)) - createTabCounterMenu(view) - - view.menuButton.setColorFilter( - ContextCompat.getColor( - requireContext(), - ThemeManager.resolveAttribute(R.attr.primaryText, requireContext()) + PerfStartup.homeFragmentOnViewCreated.measureNoInline { + super.onViewCreated(view, savedInstanceState) + context?.metrics?.track(Event.HomeScreenDisplayed) + + observeSearchEngineChanges() + createHomeMenu(requireContext(), WeakReference(view.menuButton)) + createTabCounterMenu(view) + + view.menuButton.setColorFilter( + ContextCompat.getColor( + requireContext(), + ThemeManager.resolveAttribute(R.attr.primaryText, requireContext()) + ) ) - ) - view.toolbar.compoundDrawablePadding = - view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding) - view.toolbar_wrapper.setOnClickListener { - navigateToSearch() - requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) - } - - view.toolbar_wrapper.setOnLongClickListener { - ToolbarPopupWindow.show( - WeakReference(it), - handlePasteAndGo = sessionControlInteractor::onPasteAndGo, - handlePaste = sessionControlInteractor::onPaste, - copyVisible = false - ) - true - } + view.toolbar.compoundDrawablePadding = + view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding) + view.toolbar_wrapper.setOnClickListener { + navigateToSearch() + requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) + } - view.tab_button.setOnClickListener { - openTabTray() - } + view.toolbar_wrapper.setOnLongClickListener { + ToolbarPopupWindow.show( + WeakReference(it), + handlePasteAndGo = sessionControlInteractor::onPasteAndGo, + handlePaste = sessionControlInteractor::onPaste, + copyVisible = false + ) + true + } - PrivateBrowsingButtonView( - privateBrowsingButton, - browsingModeManager - ) { newMode -> - if (newMode == BrowsingMode.Private) { - requireContext().settings().incrementNumTimesPrivateModeOpened() + view.tab_button.setOnClickListener { + openTabTray() } - if (onboarding.userHasBeenOnboarded()) { - homeFragmentStore.dispatch( - HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode)) - ) + PrivateBrowsingButtonView( + privateBrowsingButton, + browsingModeManager + ) { newMode -> + if (newMode == BrowsingMode.Private) { + requireContext().settings().incrementNumTimesPrivateModeOpened() + } + + if (onboarding.userHasBeenOnboarded()) { + homeFragmentStore.dispatch( + HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode)) + ) + } } - } - consumeFrom(requireComponents.core.store) { - updateTabCounter(it) - } + consumeFrom(requireComponents.core.store) { + updateTabCounter(it) + } - homeViewModel.sessionToDelete?.also { - if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) { - removeAllTabsAndShowSnackbar(it) - } else { - removeTabAndShowSnackbar(it) + homeViewModel.sessionToDelete?.also { + if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) { + removeAllTabsAndShowSnackbar(it) + } else { + removeTabAndShowSnackbar(it) + } } - } - homeViewModel.sessionToDelete = null + homeViewModel.sessionToDelete = null - updateTabCounter(requireComponents.core.store.state) + updateTabCounter(requireComponents.core.store.state) - if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) { - navigateToSearch() - } else if (bundleArgs.getLong(FOCUS_ON_COLLECTION, -1) >= 0) { - // No need to scroll to async'd loaded TopSites if we want to scroll to collections. - homeViewModel.shouldScrollToTopSites = false - /* Triggered when the user has added a tab to a collection and has tapped - * the View action on the [TabsTrayDialogFragment] snackbar.*/ - scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1)) + if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) { + navigateToSearch() + } else if (bundleArgs.getLong(FOCUS_ON_COLLECTION, -1) >= 0) { + // No need to scroll to async'd loaded TopSites if we want to scroll to collections. + homeViewModel.shouldScrollToTopSites = false + /* Triggered when the user has added a tab to a collection and has tapped + * the View action on the [TabsTrayDialogFragment] snackbar.*/ + scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1)) + } } - } private fun observeSearchEngineChanges() { consumeFlow(store) { flow -> @@ -531,7 +536,7 @@ class HomeFragment : Fragment() { requireContext().getString(R.string.snackbar_deleted_undo), { requireComponents.useCases.tabsUseCases.undo.invoke() - findNavController().navigate( + findNavController().navigateBlockingForAsyncNavGraph( HomeFragmentDirections.actionGlobalBrowser(null) ) }, @@ -622,7 +627,8 @@ class HomeFragment : Fragment() { } private fun navToSavedLogins() { - findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment()) + findNavController().navigateBlockingForAsyncNavGraph( + HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment()) } private fun dispatchModeChanges(mode: Mode) { @@ -738,6 +744,7 @@ class HomeFragment : Fragment() { private fun hideOnboardingAndOpenSearch() { hideOnboardingIfNeeded() + appBarLayout?.setExpanded(true, true) navigateToSearch() } @@ -777,14 +784,27 @@ class HomeFragment : Fragment() { R.id.homeFragment, HomeFragmentDirections.actionGlobalSettingsFragment() ) + requireComponents.analytics.metrics.track(Event.HomeMenuSettingsItemClicked) } - HomeMenu.Item.SyncedTabs -> { + HomeMenu.Item.SyncTabs -> { hideOnboardingIfNeeded() nav( R.id.homeFragment, HomeFragmentDirections.actionGlobalSyncedTabsFragment() ) } + is HomeMenu.Item.SyncAccount -> { + hideOnboardingIfNeeded() + val directions = if (it.signedIn) { + BrowserFragmentDirections.actionGlobalAccountSettingsFragment() + } else { + BrowserFragmentDirections.actionGlobalTurnOnSync() + } + nav( + R.id.homeFragment, + directions + ) + } HomeMenu.Item.Bookmarks -> { hideOnboardingIfNeeded() nav( @@ -841,19 +861,22 @@ class HomeFragment : Fragment() { } ) } - HomeMenu.Item.Sync -> { + HomeMenu.Item.ReconnectSync -> { hideOnboardingIfNeeded() nav( R.id.homeFragment, HomeFragmentDirections.actionGlobalAccountProblemFragment() ) } - HomeMenu.Item.AddonsManager -> { + HomeMenu.Item.Extensions -> { nav( R.id.homeFragment, HomeFragmentDirections.actionGlobalAddonsManagementFragment() ) } + is HomeMenu.Item.DesktopMode -> { + context.settings().openNextTabInDesktopMode = it.checked + } } }, onHighlightPresent = { menuButtonView.get()?.setHighlight(it) }, 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 f1284ee6a..59b25cc40 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -48,7 +48,8 @@ data class HomeFragmentState( val mode: Mode, val topSites: List, val tip: Tip? = null, - val showCollectionPlaceholder: Boolean + val showCollectionPlaceholder: Boolean, + val showSetAsDefaultBrowserCard: Boolean ) : State sealed class HomeFragmentAction : Action { @@ -69,6 +70,7 @@ sealed class HomeFragmentAction : Action { data class TopSitesChange(val topSites: List) : HomeFragmentAction() data class RemoveTip(val tip: Tip) : HomeFragmentAction() object RemoveCollectionsPlaceholder : HomeFragmentAction() + object RemoveSetDefaultBrowserCard : HomeFragmentAction() } private fun homeFragmentStateReducer( @@ -102,5 +104,6 @@ private fun homeFragmentStateReducer( is HomeFragmentAction.RemoveCollectionsPlaceholder -> { state.copy(showCollectionPlaceholder = false) } + is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = 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 6c1a32760..cf89bd2bb 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -13,15 +13,20 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuHighlight +import mozilla.components.browser.menu.BrowserMenuItem import mozilla.components.browser.menu.ext.getHighlight import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem +import mozilla.components.browser.menu.item.BrowserMenuImageSwitch import mozilla.components.browser.menu.item.BrowserMenuImageText 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.getColorFromAttr +import org.mozilla.fenix.FeatureFlags +import org.mozilla.fenix.FeatureFlags.tabsTrayRewrite import org.mozilla.fenix.R +import org.mozilla.fenix.components.accounts.FenixAccountManager import org.mozilla.fenix.experiments.ExperimentBranch import org.mozilla.fenix.experiments.Experiments import org.mozilla.fenix.ext.components @@ -30,6 +35,7 @@ import org.mozilla.fenix.ext.withExperiment import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.whatsnew.WhatsNew +@Suppress("LargeClass", "LongMethod") class HomeMenu( private val lifecycleOwner: LifecycleOwner, private val context: Context, @@ -38,26 +44,28 @@ class HomeMenu( private val onHighlightPresent: (BrowserMenuHighlight) -> Unit = {} ) { sealed class Item { + object Bookmarks : Item() + object History : Item() + object Downloads : Item() + object Extensions : Item() + object SyncTabs : Item() + data class SyncAccount(val signedIn: Boolean) : Item() object WhatsNew : Item() object Help : Item() - object AddonsManager : Item() object Settings : Item() - object SyncedTabs : Item() - object History : Item() - object Bookmarks : Item() - object Downloads : Item() object Quit : Item() - object Sync : Item() + object ReconnectSync : Item() + data class DesktopMode(val checked: Boolean) : Item() } - private val primaryTextColor = - ThemeManager.resolveAttribute(R.attr.primaryText, context) - private val syncDisconnectedColor = ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) - private val syncDisconnectedBackgroundColor = context.getColorFromAttr(R.attr.syncDisconnectedBackground) + private val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context) + private val syncDisconnectedColor = + ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) + private val syncDisconnectedBackgroundColor = + context.getColorFromAttr(R.attr.syncDisconnectedBackground) - private val menuCategoryTextColor = - ThemeManager.resolveAttribute(R.attr.menuCategoryText, context) private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar + private val accountManager = FenixAccountManager(context) // 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary. private val reconnectToSyncItem by lazy { @@ -72,7 +80,7 @@ class HomeMenu( ), isHighlighted = { true } ) { - onItemTapped.invoke(Item.Sync) + onItemTapped.invoke(Item.ReconnectSync) } } @@ -86,7 +94,34 @@ class HomeMenu( } } - private val coreMenuItems by lazy { + val syncedTabsItem = BrowserMenuImageText( + context.getString(R.string.synced_tabs), + R.drawable.ic_synced_tabs, + primaryTextColor + ) { + onItemTapped.invoke(Item.SyncTabs) + } + + private fun getSyncItemTitle(): String { + val authenticatedAccount = accountManager.authenticatedAccount + val email = accountManager.accountProfileEmail + + return if (authenticatedAccount && !email.isNullOrEmpty()) { + email + } else { + context.getString(R.string.sync_menu_sign_in) + } + } + + val syncSignInMenuItem = BrowserMenuImageText( + getSyncItemTitle(), + R.drawable.ic_synced_tabs, + primaryTextColor + ) { + onItemTapped.invoke(Item.SyncAccount(accountManager.signedInToFxa())) + } + + private val oldCoreMenuItems by lazy { val whatsNewItem = BrowserMenuHighlightableItem( context.getString(R.string.browser_menu_whats_new), R.drawable.ic_whats_new, @@ -98,17 +133,10 @@ class HomeMenu( ) { onItemTapped.invoke(Item.WhatsNew) } - val experiments = context.components.analytics.experiments - val bookmarksIcon = experiments.withExperiment(Experiments.BOOKMARK_ICON) { - when (it) { - ExperimentBranch.TREATMENT -> R.drawable.ic_bookmark_list - else -> R.drawable.ic_bookmark_filled - } - } val bookmarksItem = BrowserMenuImageText( context.getString(R.string.library_bookmarks), - bookmarksIcon, + R.drawable.ic_bookmark_list, primaryTextColor ) { onItemTapped.invoke(Item.Bookmarks) @@ -141,7 +169,7 @@ class HomeMenu( R.drawable.ic_addons_extensions, primaryTextColor ) { - onItemTapped.invoke(Item.AddonsManager) + onItemTapped.invoke(Item.Extensions) } val settingsItem = BrowserMenuImageText( @@ -152,14 +180,6 @@ class HomeMenu( onItemTapped.invoke(Item.Settings) } - val syncedTabsItem = BrowserMenuImageText( - context.getString(R.string.library_synced_tabs), - R.drawable.ic_synced_tabs, - primaryTextColor - ) { - onItemTapped.invoke(Item.SyncedTabs) - } - val helpItem = BrowserMenuImageText( context.getString(R.string.browser_menu_help), R.drawable.ic_help, @@ -211,9 +231,140 @@ class HomeMenu( } } + val desktopItem = BrowserMenuImageSwitch( + imageResource = R.drawable.ic_desktop, + label = context.getString(R.string.browser_menu_desktop_site), + initialState = { context.settings().openNextTabInDesktopMode } + ) { checked -> + onItemTapped.invoke(Item.DesktopMode(checked)) + } + + @Suppress("ComplexMethod") + private fun newCoreMenuItems(): List { + val experiments = context.components.analytics.experiments + val settings = context.components.settings + + val bookmarksItem = BrowserMenuImageText( + context.getString(R.string.library_bookmarks), + R.drawable.ic_bookmark_list, + primaryTextColor + ) { + onItemTapped.invoke(Item.Bookmarks) + } + + // We want to validate that the Nimbus experiments library is working, from the android UI + // all the way back to the data science backend. We're not testing the user's preference + // or response, we're end-to-end testing the experiments platform. + // So here, we're running multiple identical branches with the same treatment, and if the + // user isn't targeted, then we get still get the same treatment. + // The `let` block is degenerate here, but left here so as to document the form of how experiments + // are implemented here. + val historyIcon = experiments.withExperiment(Experiments.A_A_NIMBUS_VALIDATION) { + when (it) { + ExperimentBranch.A1 -> R.drawable.ic_history + ExperimentBranch.A2 -> R.drawable.ic_history + else -> R.drawable.ic_history + } + } + val historyItem = BrowserMenuImageText( + context.getString(R.string.library_history), + historyIcon, + primaryTextColor + ) { + onItemTapped.invoke(Item.History) + } + + val downloadsItem = BrowserMenuImageText( + context.getString(R.string.library_downloads), + R.drawable.ic_download, + primaryTextColor + ) { + onItemTapped.invoke(Item.Downloads) + } + + val extensionsItem = BrowserMenuImageText( + context.getString(R.string.browser_menu_add_ons), + R.drawable.ic_addons_extensions, + primaryTextColor + ) { + onItemTapped.invoke(Item.Extensions) + } + + val whatsNewItem = BrowserMenuHighlightableItem( + context.getString(R.string.browser_menu_whats_new), + R.drawable.ic_whats_new, + iconTintColorResource = primaryTextColor, + highlight = BrowserMenuHighlight.LowPriority( + notificationTint = getColor(context, R.color.whats_new_notification_color) + ), + isHighlighted = { WhatsNew.shouldHighlightWhatsNew(context) } + ) { + onItemTapped.invoke(Item.WhatsNew) + } + + val helpItem = BrowserMenuImageText( + context.getString(R.string.browser_menu_help), + R.drawable.ic_help, + primaryTextColor + ) { + onItemTapped.invoke(Item.Help) + } + + val settingsItem = BrowserMenuImageText( + context.getString(R.string.browser_menu_settings), + R.drawable.ic_settings, + primaryTextColor + ) { + onItemTapped.invoke(Item.Settings) + } + + // 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() && + context.components.backgroundServices.accountManager.accountNeedsReauth()) { + reconnectToSyncItem + } else { + null + } + + val menuItems = listOfNotNull( + bookmarksItem, + historyItem, + downloadsItem, + extensionsItem, + if (tabsTrayRewrite) syncSignInMenuItem else syncedTabsItem, + accountAuthItem, + BrowserMenuDivider(), + desktopItem, + BrowserMenuDivider(), + whatsNewItem, + helpItem, + settingsItem, + if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null + ).also { items -> + items.getHighlight()?.let { onHighlightPresent(it) } + } + + return menuItems + } + init { + val menuItems = if (FeatureFlags.toolbarMenuFeature) { + newCoreMenuItems() + } else { + oldCoreMenuItems + } + // Report initial state. - onMenuBuilderChanged(BrowserMenuBuilder(coreMenuItems)) + onMenuBuilderChanged(BrowserMenuBuilder(menuItems)) + + val menuItemsWithReconnectItem = if (FeatureFlags.toolbarMenuFeature) { + menuItems + } else { + // reconnect item is manually added to the beginning of the list + listOf(reconnectToSyncItem) + menuItems + } // Observe account state changes, and update menu item builder with a new set of items. context.components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue { @@ -224,9 +375,11 @@ class HomeMenu( context.components.backgroundServices.accountManager.register(object : AccountObserver { override fun onAuthenticationProblems() { lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { - onMenuBuilderChanged(BrowserMenuBuilder( - listOf(reconnectToSyncItem) + coreMenuItems - )) + onMenuBuilderChanged( + BrowserMenuBuilder( + menuItemsWithReconnectItem + ) + ) } } @@ -234,7 +387,7 @@ class HomeMenu( lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { onMenuBuilderChanged( BrowserMenuBuilder( - coreMenuItems + menuItems ) ) } @@ -244,7 +397,7 @@ class HomeMenu( lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { onMenuBuilderChanged( BrowserMenuBuilder( - coreMenuItems + menuItems ) ) } diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/CrashReporterIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/CrashReporterIntentProcessor.kt index dfc6feac4..7f3238e08 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/CrashReporterIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/CrashReporterIntentProcessor.kt @@ -8,6 +8,7 @@ import android.content.Intent import androidx.navigation.NavController import mozilla.components.lib.crash.Crash import org.mozilla.fenix.NavGraphDirections +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph /** * When the app crashes, the user has the option to report it. @@ -26,6 +27,6 @@ class CrashReporterIntentProcessor : HomeIntentProcessor { private fun openToCrashReporter(intent: Intent, navController: NavController) { val directions = NavGraphDirections.actionGlobalCrashReporter(intent) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt deleted file mode 100644 index 5d1893db5..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/intent/DeepLinkIntentProcessor.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.home.intent - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Build.VERSION.SDK_INT -import android.provider.Settings -import androidx.navigation.NavController -import mozilla.components.concept.engine.EngineSession -import mozilla.components.support.base.log.logger.Logger -import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.BuildConfig -import org.mozilla.fenix.GlobalDirections -import org.mozilla.fenix.HomeActivity -import org.mozilla.fenix.browser.browsingmode.BrowsingMode -import org.mozilla.fenix.components.SearchWidgetCreator -import org.mozilla.fenix.ext.alreadyOnDestination - -/** - * Deep links in the form of `fenix://host` open different parts of the app. - * - * @param verifier [DeepLinkVerifier] that will be used to verify deep links before handling them. - */ -class DeepLinkIntentProcessor( - private val activity: HomeActivity, - private val verifier: DeepLinkVerifier -) : HomeIntentProcessor { - private val logger = Logger("DeepLinkIntentProcessor") - - override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { - val scheme = intent.scheme?.equals(BuildConfig.DEEP_LINK_SCHEME, ignoreCase = true) ?: return false - return if (scheme) { - intent.data?.let { handleDeepLink(it, navController) } - true - } else { - false - } - } - - @Suppress("ComplexMethod") - private fun handleDeepLink(deepLink: Uri, navController: NavController) { - if (!verifier.verifyDeepLink(deepLink)) { - logger.warn("Invalid deep link: $deepLink") - return - } - - handleDeepLinkSideEffects(deepLink) - - val globalDirections = when (deepLink.host) { - "home", "enable_private_browsing" -> GlobalDirections.Home - "urls_bookmarks" -> GlobalDirections.Bookmarks - "urls_history" -> GlobalDirections.History - "settings" -> GlobalDirections.Settings - "turn_on_sync" -> GlobalDirections.Sync - "settings_search_engine" -> GlobalDirections.SearchEngine - "settings_accessibility" -> GlobalDirections.Accessibility - "settings_delete_browsing_data" -> GlobalDirections.DeleteData - "settings_addon_manager" -> GlobalDirections.SettingsAddonManager - "settings_logins" -> GlobalDirections.SettingsLogins - "settings_tracking_protection" -> GlobalDirections.SettingsTrackingProtection - // We'd like to highlight views within the fragment - // https://github.com/mozilla-mobile/fenix/issues/11856 - // The current version of UI has these features in more complex screens. - "settings_privacy" -> GlobalDirections.Settings - "home_collections" -> GlobalDirections.Home - - else -> return - } - - if (!navController.alreadyOnDestination(globalDirections.destinationId)) { - navController.navigate(globalDirections.navDirections) - } - } - - /** - * Handle links that require more than just simple navigation. - */ - private fun handleDeepLinkSideEffects(deepLink: Uri) { - when (deepLink.host) { - "enable_private_browsing" -> { - activity.browsingModeManager.mode = BrowsingMode.Private - } - "make_default_browser" -> { - if (SDK_INT >= Build.VERSION_CODES.N) { - val settingsIntent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - activity.startActivity(settingsIntent) - } - } - "open" -> { - val url = deepLink.getQueryParameter("url") - if (url == null || !url.startsWith("https://")) { - logger.info("Not opening deep link: $url") - return - } - - activity.openToBrowserAndLoad( - url, - newTab = true, - from = BrowserDirection.FromGlobal, - flags = EngineSession.LoadUrlFlags.external() - ) - } - "settings_notifications" -> { - val intent = notificationSettings(activity) - activity.startActivity(intent) - } - "install_search_widget" -> { - if (SDK_INT >= Build.VERSION_CODES.O) { - SearchWidgetCreator.createSearchWidget(activity) - } - } - } - } - - private fun notificationSettings(context: Context, channel: String? = null) = - Intent().apply { - when { - SDK_INT >= Build.VERSION_CODES.O -> { - action = channel?.let { - putExtra(Settings.EXTRA_CHANNEL_ID, it) - Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS - } ?: Settings.ACTION_APP_NOTIFICATION_SETTINGS - putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) - } - SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> { - action = "android.settings.APP_NOTIFICATION_SETTINGS" - putExtra("app_package", context.packageName) - putExtra("app_uid", context.applicationInfo.uid) - } - else -> { - action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - addCategory(Intent.CATEGORY_DEFAULT) - data = Uri.parse("package:" + context.packageName) - } - } - } - - /** - * Interface for a class that verifies deep links before they get handled. - */ - interface DeepLinkVerifier { - /** - * Verifies the given deep link and returns `true` for verified deep links or `false` for - * rejected deep links. - */ - fun verifyDeepLink(deepLink: Uri): Boolean - } -} 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 e8d22d4c2..74434731d 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 @@ -24,6 +24,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageVie import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder +import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.ExperimentDefaultBrowserCardViewHolder 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 @@ -116,6 +117,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { val state: OnboardingState.SignedOutCanAutoSignIn ) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID) + object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID) + object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID) object OnboardingTrackingProtection : AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID) @@ -207,6 +210,8 @@ class SessionControlAdapter( OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder( view ) + ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor) + else -> throw IllegalStateException() } } 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 8339d8c87..318dcb163 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 @@ -39,6 +39,7 @@ import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav +import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentDirections @@ -167,6 +168,16 @@ interface SessionControlController { * @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened] */ fun handleMenuOpened() + + /** + * @see [ExperimentCardInteractor.onSetDefaultBrowserClicked] + */ + fun handleSetDefaultBrowser() + + /** + * @see [ExperimentCardInteractor.onCloseExperimentCardClicked] + */ + fun handleCloseExperimentCard() } @Suppress("TooManyFunctions", "LargeClass") @@ -181,6 +192,7 @@ class DefaultSessionControlController( private val restoreUseCase: TabsUseCases.RestoreUseCase, private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase, private val selectTabUseCase: TabsUseCases.SelectTabUseCase, + private val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase, private val fragmentStore: HomeFragmentStore, private val navController: NavController, private val viewLifecycleScope: CoroutineScope, @@ -403,6 +415,10 @@ class DefaultSessionControlController( selectTab = true, startLoading = true ) + + if (settings.openNextTabInDesktopMode) { + activity.handleRequestDesktopMode() + } activity.openToBrowser(BrowserDirection.FromHome) } @@ -548,4 +564,16 @@ class DefaultSessionControlController( ) navController.nav(R.id.homeFragment, directions) } + + override fun handleSetDefaultBrowser() { + settings.userDismissedExperimentCard = true + metrics.track(Event.SetDefaultBrowserNewTabClicked) + activity.openSetDefaultBrowserOption() + } + + override fun handleCloseExperimentCard() { + settings.userDismissedExperimentCard = true + metrics.track(Event.CloseExperimentCardClicked) + fragmentStore.dispatch(HomeFragmentAction.RemoveSetDefaultBrowserCard) + } } 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 33bdd0f31..c88fa0fc1 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 @@ -190,6 +190,18 @@ interface TopSiteInteractor { fun onTopSiteMenuOpened() } +interface ExperimentCardInteractor { + /** + * Called when set default browser button is clicked + */ + fun onSetDefaultBrowserClicked() + + /** + * Called when close button on experiment card + */ + fun onCloseExperimentCardClicked() +} + /** * Interactor for the Home screen. * Provides implementations for the CollectionInteractor, OnboardingInteractor, @@ -199,7 +211,7 @@ interface TopSiteInteractor { class SessionControlInteractor( private val controller: SessionControlController ) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, - TabSessionInteractor, ToolbarInteractor { + TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor { override fun onCollectionAddTabTapped(collection: TabCollection) { controller.handleCollectionAddTabTapped(collection) } @@ -295,4 +307,12 @@ class SessionControlInteractor( override fun onTopSiteMenuOpened() { controller.handleMenuOpened() } + + override fun onSetDefaultBrowserClicked() { + controller.handleSetDefaultBrowser() + } + + override fun onCloseExperimentCardClicked() { + controller.handleCloseExperimentCard() + } } 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 3258fb31d..859d23d1a 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 @@ -12,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.android.extensions.LayoutContainer 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 @@ -22,18 +21,23 @@ import org.mozilla.fenix.home.OnboardingState // This method got a little complex with the addition of the tab tray feature flag // When we remove the tabs from the home screen this will get much simpler again. -@Suppress("ComplexMethod") +@Suppress("ComplexMethod", "LongParameterList") private fun normalModeAdapterItems( topSites: List, collections: List, expandedCollections: Set, tip: Tip?, - showCollectionsPlaceholder: Boolean + showCollectionsPlaceholder: Boolean, + showSetAsDefaultBrowserCard: Boolean ): List { val items = mutableListOf() tip?.let { items.add(AdapterItem.TipItem(it)) } + if (showSetAsDefaultBrowserCard) { + items.add(AdapterItem.ExperimentDefaultBrowserCard) + } + if (topSites.isNotEmpty()) { items.add(AdapterItem.TopSitePager(topSites)) } @@ -71,6 +75,13 @@ private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescri private fun onboardingAdapterItems(onboardingState: OnboardingState): List { val items: MutableList = mutableListOf(AdapterItem.OnboardingHeader) + items.addAll( + listOf( + AdapterItem.OnboardingThemePicker, + AdapterItem.OnboardingToolbarPositionPicker, + AdapterItem.OnboardingTrackingProtection + ) + ) // Customize FxA items based on where we are with the account state: items.addAll( when (onboardingState) { @@ -90,14 +101,6 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List = when (mode) { collections, expandedCollections, tip, - showCollectionPlaceholder + showCollectionPlaceholder, + showSetAsDefaultBrowserCard ) is Mode.Private -> privateModeAdapterItems() is Mode.Onboarding -> onboardingAdapterItems(mode.state) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt new file mode 100644 index 000000000..27476132e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/ExperimentDefaultBrowserCardViewHolder.kt @@ -0,0 +1,36 @@ +/* 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.onboarding + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.experiment_default_browser.view.* +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.increaseTapArea +import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor + +class ExperimentDefaultBrowserCardViewHolder( + view: View, + private val interactor: SessionControlInteractor +) : RecyclerView.ViewHolder(view) { + + init { + view.set_default_browser.setOnClickListener { + interactor.onSetDefaultBrowserClicked() + } + + view.close.apply { + increaseTapArea(CLOSE_BUTTON_EXTRA_DPS) + setOnClickListener { + interactor.onCloseExperimentCardClicked() + } + } + } + + companion object { + internal const val LAYOUT_ID = R.layout.experiment_default_browser + private const val CLOSE_BUTTON_EXTRA_DPS = 38 + } +} 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 98155f061..a40cc55de 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 @@ -10,35 +10,26 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.onboarding_manual_signin.view.* 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.ext.navigateBlockingForAsyncNavGraph 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 { - 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() + Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions) } } fun bind() { val context = itemView.context - headerText.text = context.getString(R.string.onboarding_account_sign_in_header) + headerText.text = context.getString(R.string.onboarding_account_sign_in_header_1) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt index 668c17cee..9f4d752c7 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt @@ -18,7 +18,7 @@ class OnboardingPrivacyNoticeViewHolder( ) : RecyclerView.ViewHolder(view) { init { - view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_privacy_notice) + view.header_text.setOnboardingIcon(R.drawable.ic_info) val appName = view.context.getString(R.string.app_name) view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description2, appName) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolder.kt index 9d2e00013..7c3b52610 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingTrackingProtectionViewHolder.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding import android.view.View -import androidx.appcompat.widget.SwitchCompat import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.* import org.mozilla.fenix.R @@ -20,29 +19,20 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold private var standardTrackingProtection: OnboardingRadioButton private var strictTrackingProtection: OnboardingRadioButton - private var trackingProtectionToggle: SwitchCompat init { view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_tracking_protection) - trackingProtectionToggle = view.tracking_protection_toggle standardTrackingProtection = view.tracking_protection_standard_option strictTrackingProtection = view.tracking_protection_strict_default view.description_text.text = view.context.getString( - R.string.onboarding_tracking_protection_description_2 + R.string.onboarding_tracking_protection_description_3 ) - trackingProtectionToggle.apply { - isChecked = view.context.settings().shouldUseTrackingProtection - setOnCheckedChangeListener { _, isChecked -> - updateTrackingProtectionSetting(isChecked) - updateRadioGroupState(isChecked) - } - } - - setupRadioGroup(trackingProtectionToggle.isChecked) - updateRadioGroupState(trackingProtectionToggle.isChecked) + val isTrackingProtectionEnabled = view.context.settings().shouldUseTrackingProtection + setupRadioGroup(isTrackingProtectionEnabled) + updateRadioGroupState(isTrackingProtectionEnabled) } private fun setupRadioGroup(isChecked: Boolean) { @@ -74,15 +64,6 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold strictTrackingProtection.isEnabled = isChecked } - private fun updateTrackingProtectionSetting(enabled: Boolean) { - itemView.context.settings().shouldUseTrackingProtection = enabled - with(itemView.context.components) { - val policy = core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy() - useCases.settingsUseCases.updateTrackingProtection.invoke(policy) - useCases.sessionUseCases.reload.invoke() - } - } - private fun updateTrackingProtectionPolicy() { itemView.context?.components?.let { val policy = it.core.trackingProtectionPolicyFactory 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 48c4ee3e1..5ee55eb98 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,7 @@ 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 mozilla.components.feature.top.sites.TopSite.Type.PINNED import org.mozilla.fenix.R @@ -63,7 +64,7 @@ class TopSiteItemViewHolder( fun bind(topSite: TopSite) { top_site_title.text = topSite.title - pin_indicator.visibility = if (topSite.type == PINNED) { + pin_indicator.visibility = if (topSite.type == PINNED || topSite.type == DEFAULT) { View.VISIBLE } else { View.GONE @@ -79,6 +80,9 @@ class TopSiteItemViewHolder( SupportUtils.JD_URL -> { favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_jd)) } + SupportUtils.PDD_URL -> { + favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_pdd)) + } else -> { itemView.context.components.core.icons.loadIntoView(favicon_image, topSite.url) } diff --git a/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt b/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt index c444c0489..67ade043d 100644 --- a/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/LibrarySiteItemView.kt @@ -19,34 +19,8 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.loadIntoView - -/** - * Interactor for items that can be selected on the bookmarks and history screens. - */ -interface SelectionInteractor { - /** - * Called when an item is tapped to open it. - * @param item the tapped item to open. - */ - fun open(item: T) - - /** - * Called when an item is long pressed and selection mode is started, - * or when selection mode has already started an an item is tapped. - * @param item the item to select. - */ - fun select(item: T) - - /** - * Called when a selected item is tapped in selection mode and should no longer be selected. - * @param item the item to deselect. - */ - fun deselect(item: T) -} - -interface SelectionHolder { - val selectedItems: Set -} +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.selection.SelectionInteractor class LibrarySiteItemView @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt index 0b80684e4..3dc18e26d 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkController.kt @@ -88,7 +88,7 @@ class DefaultBookmarkController( } override fun handleBookmarkEdit(node: BookmarkNode) { - navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid)) + navigateToGivenDirection(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid)) } override fun handleBookmarkSelected(node: BookmarkNode) { @@ -118,7 +118,7 @@ class DefaultBookmarkController( } override fun handleBookmarkSharing(item: BookmarkNode) { - navigate( + navigateToGivenDirection( BookmarkFragmentDirections.actionGlobalShareFragment( data = arrayOf(ShareData(url = item.url, title = item.title)) ) @@ -182,7 +182,7 @@ class DefaultBookmarkController( } } - private fun navigate(directions: NavDirections) { + private fun navigateToGivenDirection(directions: NavDirections) { invokePendingDeletion.invoke() navController.nav(R.id.bookmarkFragment, directions) } diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 8769e35af..4b93a4f31 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -202,7 +202,7 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan true } R.id.add_bookmark_folder -> { - navigate( + navigateToBookmarkFragment( BookmarkFragmentDirections .actionBookmarkFragmentToBookmarkAddFolderFragment() ) @@ -226,7 +226,7 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan val shareTabs = bookmarkStore.state.mode.selectedItems.map { ShareData(url = it.url, title = it.title) } - navigate( + navigateToBookmarkFragment( BookmarkFragmentDirections.actionGlobalShareFragment( data = shareTabs.toTypedArray() ) @@ -243,10 +243,10 @@ class BookmarkFragment : LibraryPageFragment(), UserInteractionHan private fun showTabTray() { invokePendingDeletion() - navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment()) + navigateToBookmarkFragment(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment()) } - private fun navigate(directions: NavDirections) { + private fun navigateToBookmarkFragment(directions: NavDirections) { invokePendingDeletion() findNavController().nav( R.id.bookmarkFragment, diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentStore.kt index da5a14ae2..26901ddeb 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragmentStore.kt @@ -9,7 +9,7 @@ import mozilla.components.concept.storage.BookmarkNode import mozilla.components.lib.state.Action import mozilla.components.lib.state.State import mozilla.components.lib.state.Store -import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.selection.SelectionHolder class BookmarkFragmentStore( initialState: BookmarkFragmentState diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkView.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkView.kt index 5887a871d..d08bfd97e 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkView.kt @@ -15,8 +15,9 @@ import mozilla.components.concept.storage.BookmarkNode import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.library.LibraryPageView -import org.mozilla.fenix.library.SelectionInteractor +import org.mozilla.fenix.selection.SelectionInteractor /** * Interface for the Bookmarks view. @@ -119,7 +120,7 @@ class BookmarkView( adapter = bookmarkAdapter } view.bookmark_folders_sign_in.setOnClickListener { - navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) + navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalTurnOnSync()) } view.swipe_refresh.setOnRefreshListener { interactor.onRequestSync() 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 index 043053049..a24ccef52 100644 --- a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadAdapter.kt @@ -8,7 +8,7 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView -import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder class DownloadAdapter( 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 index 82ad310b1..b122ef8f0 100644 --- a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadView.kt @@ -17,7 +17,7 @@ import kotlinx.android.synthetic.main.component_history.view.swipe_refresh import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.R import org.mozilla.fenix.library.LibraryPageView -import org.mozilla.fenix.library.SelectionInteractor +import org.mozilla.fenix.selection.SelectionInteractor /** * Interface for the DownloadViewInteractor. This interface is implemented by objects that want 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 index 46e76067d..5a80e3719 100644 --- 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 @@ -11,7 +11,7 @@ import kotlinx.android.synthetic.main.download_list_item.view.* import kotlinx.android.synthetic.main.library_site_item.view.* import mozilla.components.feature.downloads.toMegabyteOrKilobyteString import org.mozilla.fenix.R -import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.library.downloads.DownloadInteractor import org.mozilla.fenix.library.downloads.DownloadItem import org.mozilla.fenix.ext.getIcon diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt index e82bac914..32b43b221 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryAdapter.kt @@ -11,7 +11,7 @@ import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import org.mozilla.fenix.R -import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.library.history.viewholders.HistoryListItemViewHolder import java.util.Calendar import java.util.Date diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt index 96ef57698..8d9114a43 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryController.kt @@ -17,6 +17,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph @Suppress("TooManyFunctions") interface HistoryController { @@ -94,7 +95,7 @@ class DefaultHistoryController( } override fun handleShare(item: HistoryItem) { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( HistoryFragmentDirections.actionGlobalShareFragment( data = arrayOf(ShareData(url = item.url, title = item.title)) ) @@ -110,7 +111,7 @@ class DefaultHistoryController( } override fun handleEnterRecentlyClosed() { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( HistoryFragmentDirections.actionGlobalRecentlyClosed(), NavOptions.Builder().setPopUpTo(R.id.recentlyClosedFragment, true).build() ) diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index e3a826621..7d96b91b7 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -304,10 +304,10 @@ class HistoryFragment : LibraryPageFragment(), UserInteractionHandl val directions = HistoryFragmentDirections.actionGlobalShareFragment( data = data.toTypedArray() ) - navigate(directions) + navigateToHistoryFragment(directions) } - private fun navigate(directions: NavDirections) { + private fun navigateToHistoryFragment(directions: NavDirections) { invokePendingDeletion() findNavController().nav( R.id.historyFragment, diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt index 3bfb5076a..5afbdc3d6 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryView.kt @@ -17,7 +17,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.library.LibraryPageView -import org.mozilla.fenix.library.SelectionInteractor +import org.mozilla.fenix.selection.SelectionInteractor import org.mozilla.fenix.theme.ThemeManager /** diff --git a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt index 82a6e0eaa..2198713a9 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/viewholders/HistoryListItemViewHolder.kt @@ -15,7 +15,7 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.hideAndDisable import org.mozilla.fenix.ext.showAndEnable -import org.mozilla.fenix.library.SelectionHolder +import org.mozilla.fenix.selection.SelectionHolder import org.mozilla.fenix.library.history.HistoryFragmentState import org.mozilla.fenix.library.history.HistoryInteractor import org.mozilla.fenix.library.history.HistoryItem diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt index a4461a246..47f1b2935 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt @@ -19,6 +19,7 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.components.FenixSnackbar +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph interface RecentlyClosedController { fun handleOpen(item: RecoverableTab, mode: BrowsingMode? = null) @@ -48,7 +49,7 @@ class DefaultRecentlyClosedController( } override fun handleNavigateToHistory() { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( RecentlyClosedFragmentDirections.actionGlobalHistoryFragment(), NavOptions.Builder().setPopUpTo(R.id.historyFragment, true).build() ) @@ -64,7 +65,7 @@ class DefaultRecentlyClosedController( } override fun handleShare(item: RecoverableTab) { - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( RecentlyClosedFragmentDirections.actionGlobalShareFragment( data = arrayOf(ShareData(url = item.url, title = item.title)) ) diff --git a/app/src/main/java/org/mozilla/fenix/nimbus/NimbusDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusDetailsFragment.kt new file mode 100644 index 000000000..7caf576bd --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusDetailsFragment.kt @@ -0,0 +1,59 @@ +/* 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.nimbus + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.service.nimbus.ui.NimbusDetailAdapter +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.showToolbar + +/** + * A fragment to show the details of a Nimbus experiment. + */ +class NimbusDetailsFragment : Fragment(R.layout.mozac_service_nimbus_experiment_details) { + + private val args by navArgs() + private var adapter: NimbusDetailAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + bindRecyclerView(view) + } + + override fun onResume() { + super.onResume() + showToolbar(args.experiment) + } + + override fun onDestroyView() { + super.onDestroyView() + // Letting go of the resources to avoid memory leak. + adapter = null + } + + private fun bindRecyclerView(view: View) { + val recyclerView = view.findViewById(R.id.nimbus_experiment_branches_list) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + val shouldRefresh = adapter != null + + // Dummy data until we have the appropriate Nimbus API. + val branches = listOf( + "Control", + "Treatment" + ) + + if (!shouldRefresh) { + adapter = NimbusDetailAdapter(branches) + } + + recyclerView.adapter = adapter + } +} diff --git a/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsFragment.kt b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsFragment.kt new file mode 100644 index 000000000..4a8ec904f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsFragment.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.nimbus + +import android.os.Bundle +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import mozilla.components.service.nimbus.ui.NimbusExperimentAdapter +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.runIfFragmentIsAttached +import org.mozilla.fenix.ext.showToolbar + +/** + * Fragment use for managing Nimbus experiments. + */ +@Suppress("TooGenericExceptionCaught") +class NimbusExperimentsFragment : Fragment(R.layout.mozac_service_nimbus_experiments) { + + private var adapter: NimbusExperimentAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + bindRecyclerView(view) + } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.preferences_nimbus_experiments)) + } + + override fun onDestroyView() { + super.onDestroyView() + // Letting go of the resources to avoid memory leak. + adapter = null + } + + private fun bindRecyclerView(view: View) { + val experimentsView = NimbusExperimentsView( + navController = findNavController() + ) + + val recyclerView = view.findViewById(R.id.nimbus_experiments_list) + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + + val shouldRefresh = adapter != null + + lifecycleScope.launch(IO) { + try { + val experiments = + requireContext().components.analytics.experiments.getActiveExperiments() + + lifecycleScope.launch(Main) { + runIfFragmentIsAttached { + if (!shouldRefresh) { + adapter = NimbusExperimentAdapter( + experimentsView, + experiments + ) + } + + view.findViewById(R.id.nimbus_experiments_empty_message).isVisible = + false + recyclerView.adapter = adapter + } + } + } catch (e: Throwable) { + Logger.error("Failed to getActiveExperiments()", e) + view.findViewById(R.id.nimbus_experiments_empty_message).isVisible = true + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsView.kt b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsView.kt new file mode 100644 index 000000000..1bc6e030f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/nimbus/NimbusExperimentsView.kt @@ -0,0 +1,27 @@ +/* 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.nimbus + +import androidx.navigation.NavController +import mozilla.components.service.nimbus.ui.NimbusExperimentsAdapterDelegate +import org.mozilla.experiments.nimbus.EnrolledExperiment +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph + +/** + * View used for managing Nimbus experiments. + */ +class NimbusExperimentsView( + private val navController: NavController +) : NimbusExperimentsAdapterDelegate { + + override fun onExperimentItemClicked(experiment: EnrolledExperiment) { + val directions = + NimbusExperimentsFragmentDirections.actionNimbusExperimentsFragmentToNimbusDetailsFragment( + experiment.userFacingName + ) + + navController.navigateBlockingForAsyncNavGraph(directions) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt b/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt deleted file mode 100644 index 3e1945103..000000000 --- a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingController.kt +++ /dev/null @@ -1,22 +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.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 deleted file mode 100644 index ad11d459c..000000000 --- a/app/src/main/java/org/mozilla/fenix/onboarding/OnboardingInteractor.kt +++ /dev/null @@ -1,14 +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.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/perf/AppStartReasonProvider.kt b/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt new file mode 100644 index 000000000..1b54f1589 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/AppStartReasonProvider.kt @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.app.Activity +import android.app.Application +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.GleanMetrics.Metrics +import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks + +private val logger = Logger("AppStartReasonProvider") + +/** + * Provides the reason this [Application] instance was started: see [StartReason] for options + * and [reason] for details. + * + * [registerInAppOnCreate] must be called for this class to work correctly. + * + * This class relies on specific lifecycle method call orders and main thread Runnable scheduling + * that could potentially change between OEMs and OS versions: **be careful when using it.** This + * implementation was tested on the Moto G5 Android 8.1.0 and the Pixel 2 Android 11. + */ +class AppStartReasonProvider { + + enum class StartReason { + /** We don't know yet what caused this [Application] instance to be started. */ + TO_BE_DETERMINED, + + /** This [Application] instance was started due to an Activity trying to start. */ + ACTIVITY, + + /** + * This [Application] instance was started due to a component that is not an Activity: + * this may include Services, BroadcastReceivers, and ContentProviders. It may be possible + * to distinguish between these but it hasn't been necessary to do so yet. + */ + NON_ACTIVITY, + } + + /** + * The reason this [Application] instance was started. This will not be set immediately + * but is expected to be available by the time the first frame is drawn for the foreground Activity. + */ + var reason = StartReason.TO_BE_DETERMINED + private set + + /** + * Registers the handlers needed by this class: this is expected to be called from + * [Application.onCreate]. + */ + fun registerInAppOnCreate(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(ProcessLifecycleObserver()) + application.registerActivityLifecycleCallbacks(ActivityLifecycleCallbacks()) + } + + private inner class ProcessLifecycleObserver : DefaultLifecycleObserver { + override fun onCreate(owner: LifecycleOwner) { + Handler(Looper.getMainLooper()).post { + // If the Application was started by an Activity, this Runnable should execute + // after we learn the Activity was created. If the App was started by a Service, + // this Runnable should execute before the first Activity is created. + reason = when (reason) { + StartReason.TO_BE_DETERMINED -> StartReason.NON_ACTIVITY + StartReason.ACTIVITY -> reason /* the start reason is already known: do nothing. */ + StartReason.NON_ACTIVITY -> { + Metrics.startReasonProcessError.set(true) + logger.error("AppStartReasonProvider.Process...onCreate unexpectedly called twice") + reason + } + } + } + + owner.lifecycle.removeObserver(this) // we don't update the state further. + } + } + + private inner class ActivityLifecycleCallbacks : DefaultActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + // See ProcessLifecycleObserver.onCreate for details. + reason = when (reason) { + StartReason.TO_BE_DETERMINED -> StartReason.ACTIVITY + StartReason.NON_ACTIVITY -> reason /* the start reason is already known: do nothing. */ + StartReason.ACTIVITY -> { + Metrics.startReasonActivityError.set(true) + logger.error("AppStartReasonProvider.Activity...onCreate unexpectedly called twice") + reason + } + } + + activity.application.unregisterActivityLifecycleCallbacks(this) // we don't update the state further. + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/ColdStartupDurationTelemetry.kt b/app/src/main/java/org/mozilla/fenix/perf/ColdStartupDurationTelemetry.kt new file mode 100644 index 000000000..28d757c26 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/ColdStartupDurationTelemetry.kt @@ -0,0 +1,72 @@ +/* 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.content.Intent +import android.os.SystemClock +import android.view.View +import androidx.core.view.doOnPreDraw +import mozilla.components.support.base.log.logger.Logger +import mozilla.components.support.utils.SafeIntent +import org.mozilla.fenix.GleanMetrics.PerfStartup +import org.mozilla.fenix.HomeActivity +import java.util.concurrent.TimeUnit + +private val logger = Logger("ColdStartupDuration") + +/** + * A class to record COLD start up telemetry. This class is intended to improve upon our mistakes from the + * [org.mozilla.fenix.components.metrics.AppStartupTelemetry] class by being simple-to-implement and + * simple-to-analyze (i.e. works in GLAM) rather than being a "perfect" and comprehensive measurement. + * + * This class relies on external state providers like [StartupStateProvider] that are tricky to + * implement correctly so take the results with a grain of salt. + */ +class ColdStartupDurationTelemetry { + + fun onHomeActivityOnCreate( + visualCompletenessQueue: VisualCompletenessQueue, + startupStateProvider: StartupStateProvider, + safeIntent: SafeIntent, + rootContainer: View + ) { + // Optimization: it might be expensive to post runnables so we can short-circuit + // with a subset of the later logic. + if (startupStateProvider.shouldShortCircuitColdStart()) { + logger.debug("Not measuring: is not cold start (short-circuit)") + return + } + + rootContainer.doOnPreDraw { + // This block takes 0ms on a Moto G5: it doesn't seem long enough to optimize. + val firstFrameNanos = SystemClock.elapsedRealtimeNanos() + if (startupStateProvider.isColdStartForStartedActivity(HomeActivity::class.java)) { + visualCompletenessQueue.queue.runIfReadyOrQueue { + recordColdStartupTelemetry(safeIntent, firstFrameNanos) + } + } + } + } + + private fun recordColdStartupTelemetry(safeIntent: SafeIntent, firstFrameNanos: Long) { + // This code duplicates the logic for determining how we should handle this intent which + // could result in inconsistent results: e.g. the browser might get a VIEW intent but it's + // malformed so the app treats it as a MAIN intent but here we record VIEW. However, the + // logic for determining the end state is distributed and buried & inspecting the end state + // is fragile (e.g. if the browser was open, was it a MAIN w/ session restore or VIEW?) so we + // use this simpler solution even if it's imperfect. Hopefully, the success cases will + // outnumber the edge cases into statistical insignificance. + val (metric, typeForLog) = when (safeIntent.action) { + Intent.ACTION_MAIN -> Pair(PerfStartup.coldMainAppToFirstFrame, "MAIN") + Intent.ACTION_VIEW -> Pair(PerfStartup.coldViewAppToFirstFrame, "VIEW") + else -> Pair(PerfStartup.coldUnknwnAppToFirstFrame, "UNKNOWN") + } + + val startNanos = StartupTimeline.frameworkStartMeasurement.applicationInitNanos + val durationMillis = TimeUnit.NANOSECONDS.toMillis(firstFrameNanos - startNanos) + metric.accumulateSamples(longArrayOf(durationMillis)) + logger.info("COLD $typeForLog Application. to first frame: $durationMillis ms") + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/NavGraphProvider.kt b/app/src/main/java/org/mozilla/fenix/perf/NavGraphProvider.kt new file mode 100644 index 000000000..a6b60a5ca --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/NavGraphProvider.kt @@ -0,0 +1,59 @@ +/* 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 androidx.lifecycle.LifecycleCoroutineScope +import androidx.navigation.NavController +import java.util.WeakHashMap +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import org.mozilla.fenix.R + +/** + * This class asynchronously loads the navigation graph XML. This is a performance optimization: + * large nav graphs take ~29ms to inflate on the Moto G5 (#16900). This also seemingly prevents the + * HomeFragment layout XML from being unnecessarily inflated when it isn't used, improving perf by + * ~148ms on the Moto G5 for VIEW start up (#18245) though it was unintentional and we may wish to + * implement more intentional for that. + * + * This class is defined as an Object, rather than as a class instance in our Components, because + * it needs to be called by the [NavController] extension function which can't easily access Components. + * + * To use this class properly, [inflateNavGraphAsync] must be called first before + * [blockForNavGraphInflation] using the same [NavController] instance. + */ +object NavGraphProvider { + + // We want to store member state on the NavController. However, there is no way to do this. + // Instead, we store state as part of a map: NavController instance -> State. In order to + // garbage collect our data when the NavController is no longer relevant, we use a WeakHashMap. + private val map = WeakHashMap() + + fun inflateNavGraphAsync(navController: NavController, lifecycleScope: LifecycleCoroutineScope) { + val inflationJob = lifecycleScope.launch(Dispatchers.IO) { + val inflater = navController.navInflater + navController.graph = inflater.inflate(R.navigation.nav_graph) + } + + map[navController] = inflationJob + } + + /** + * The job should block the main thread if it isn't completed so that the NavGraph can be loaded + * before any navigation is done. + * + * [inflateNavGraphAsync] must be called before this method. + * + * @throws IllegalStateException if [inflateNavGraphAsync] wasn't called first for this [NavController] + */ + fun blockForNavGraphInflation(navController: NavController) { + val inflationJob = map[navController] ?: throw IllegalStateException("Expected " + + "`NavGraphProvider.inflateNavGraphAsync` to be called before this method with the same " + + "`NavController` instance. If this occurred in a test, you probably need to add the " + + "DisableNavGraphProviderAssertionRule.") + runBlockingIncrement { inflationJob.join() } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupActivityLog.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupActivityLog.kt new file mode 100644 index 000000000..279a576de --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupActivityLog.kt @@ -0,0 +1,105 @@ +/* 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.app.Activity +import android.app.Application +import android.os.Bundle +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.NONE +import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import mozilla.components.support.base.log.Log +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.android.DefaultActivityLifecycleCallbacks + +private val logger = Logger("StartupActivityLog") + +/** + * A record of the [Activity] created, started, and stopped events as well as [Application] + * foreground and background events. See [log] for the log. This class is expected to be + * registered in [Application.onCreate] by calling [registerInAppOnCreate]. + * + * To prevent this list from growing infinitely, we clear the list when the application is stopped. + * This is acceptable from the current requirements: we never need to inspect more than the current + * start up. + */ +class StartupActivityLog { + + private val _log = mutableListOf() + val log: List = _log + + fun registerInAppOnCreate( + application: Application, + processLifecycleOwner: LifecycleOwner = ProcessLifecycleOwner.get() + ) { + processLifecycleOwner.lifecycle.addObserver(StartupLogAppLifecycleObserver()) + application.registerActivityLifecycleCallbacks(StartupLogActivityLifecycleCallbacks()) + } + + @VisibleForTesting(otherwise = NONE) + fun getObserversForTesting() = Pair(StartupLogAppLifecycleObserver(), StartupLogActivityLifecycleCallbacks()) + + @VisibleForTesting(otherwise = PRIVATE) + fun logEntries(loggerArg: Logger = logger, logLevel: Log.Priority = Log.logLevel) { + // Optimization: we want to avoid the potentially expensive conversions + // to Strings if we're not going to log anyway. + if (logLevel > Log.Priority.DEBUG) { + return + } + + val transformedEntries = log.map { when (it) { + is LogEntry.AppStarted -> "App-STARTED" + is LogEntry.AppStopped -> "App-STOPPED" + is LogEntry.ActivityCreated -> "${it.activityClass.simpleName}-CREATED" + is LogEntry.ActivityStarted -> "${it.activityClass.simpleName}-STARTED" + is LogEntry.ActivityStopped -> "${it.activityClass.simpleName}-STOPPED" + } } + + loggerArg.debug(transformedEntries.toString()) + } + + @VisibleForTesting(otherwise = PRIVATE) + inner class StartupLogAppLifecycleObserver : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + _log.add(LogEntry.AppStarted) + } + + override fun onStop(owner: LifecycleOwner) { + logEntries() + _log.clear() // Optimization: see class kdoc for details. + _log.add(LogEntry.AppStopped) + } + } + + @VisibleForTesting(otherwise = PRIVATE) + inner class StartupLogActivityLifecycleCallbacks : DefaultActivityLifecycleCallbacks { + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + _log.add(LogEntry.ActivityCreated(activity::class.java)) + } + + override fun onActivityStarted(activity: Activity) { + _log.add(LogEntry.ActivityStarted(activity::class.java)) + } + + override fun onActivityStopped(activity: Activity) { + _log.add(LogEntry.ActivityStopped(activity::class.java)) + } + } + + /** + * A log entry with its detailed information for the [StartupActivityLog]. + */ + sealed class LogEntry { + object AppStarted : LogEntry() + object AppStopped : LogEntry() + + data class ActivityCreated(val activityClass: Class) : LogEntry() + data class ActivityStarted(val activityClass: Class) : LogEntry() + data class ActivityStopped(val activityClass: Class) : LogEntry() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt index a63f0bb61..359ff44f6 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt @@ -26,7 +26,8 @@ internal class StartupFrameworkStartMeasurement( private var isMetricSet = false - private var applicationInitNanos = -1L + var applicationInitNanos = -1L + private set private var isApplicationInitCalled = false fun onApplicationInit() { diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupPathProvider.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupPathProvider.kt new file mode 100644 index 000000000..f6b6f375f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupPathProvider.kt @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.app.Activity +import android.content.Intent +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.NONE +import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner + +/** + * The "path" that this activity started in. See the + * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + * for specific definitions. + * + * This should be a member variable of [Activity] because its data is tied to the lifecycle of an + * Activity. Call [attachOnActivityOnCreate] & [onIntentReceived] for this class to work correctly. + */ +class StartupPathProvider { + + /** + * The path the application took to + * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + * for specific definitions. + */ + enum class StartupPath { + MAIN, + VIEW, + + /** + * The start up path if we received an Intent but we're unable to categorize it into other buckets. + */ + UNKNOWN, + + /** + * The start up path has not been set. This state includes: + * - this API is accessed before it is set + * - if no intent is received before the activity is STARTED (e.g. app switcher) + */ + NOT_SET + } + + /** + * Returns the [StartupPath] for the currently started activity. This value will be set + * after an [Intent] is received that causes this activity to move into the STARTED state. + */ + var startupPathForActivity = StartupPath.NOT_SET + private set + + private var wasResumedSinceStartedState = false + + fun attachOnActivityOnCreate(lifecycle: Lifecycle, intent: Intent?) { + lifecycle.addObserver(StartupPathLifecycleObserver()) + onIntentReceived(intent) + } + + // N.B.: this method duplicates the actual logic for determining what page to open. + // Unfortunately, it's difficult to re-use that logic because it occurs in many places throughout + // the code so we do the simple thing for now and duplicate it. It's noticeably different from + // what you might expect: e.g. ACTION_MAIN can open a URL and if ACTION_VIEW provides an invalid + // URL, it'll perform a MAIN action. However, it's fairly representative of what users *intended* + // to do when opening the app and shouldn't change much because it's based on Android system-wide + // conventions, so it's probably fine for our purposes. + private fun getStartupPathFromIntent(intent: Intent): StartupPath = when (intent.action) { + Intent.ACTION_MAIN -> StartupPath.MAIN + Intent.ACTION_VIEW -> StartupPath.VIEW + else -> StartupPath.UNKNOWN + } + + /** + * Expected to be called when a new [Intent] is received by the [Activity]: i.e. + * [Activity.onCreate] and [Activity.onNewIntent]. + */ + fun onIntentReceived(intent: Intent?) { + // We want to set a path only if the intent causes the Activity to move into the STARTED state. + // This means we want to discard any intents that are received when the app is foregrounded. + // However, we can't use the Lifecycle.currentState to determine this because: + // - the app is briefly paused (state becomes STARTED) before receiving the Intent in + // the foreground so we can't say <= STARTED + // - onIntentReceived can be called from the CREATED or STARTED state so we can't say == CREATED + // So we're forced to track this state ourselves. + if (!wasResumedSinceStartedState && intent != null) { + startupPathForActivity = getStartupPathFromIntent(intent) + } + } + + @VisibleForTesting(otherwise = NONE) + fun getTestCallbacks() = StartupPathLifecycleObserver() + + @VisibleForTesting(otherwise = PRIVATE) + inner class StartupPathLifecycleObserver : DefaultLifecycleObserver { + override fun onResume(owner: LifecycleOwner) { + wasResumedSinceStartedState = true + } + + override fun onStop(owner: LifecycleOwner) { + // Clear existing state. + startupPathForActivity = StartupPath.NOT_SET + wasResumedSinceStartedState = false + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupStateProvider.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupStateProvider.kt new file mode 100644 index 000000000..e43223bfd --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupStateProvider.kt @@ -0,0 +1,154 @@ +/* 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.app.Activity +import org.mozilla.fenix.perf.AppStartReasonProvider.StartReason +import org.mozilla.fenix.perf.StartupActivityLog.LogEntry + +/** + * Identifies the "state" of start up where state can be COLD/WARM/HOT and possibly others. See + * the [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + * for specific definitions. + * + * This class is nuanced: **please read the kdoc carefully before using it.** Consider contacting + * the perf team with your use case. + * + * For this class, we use the terminology from the [StartupActivityLog] such as STARTED and STOPPED. + * However, we're assuming STARTED means foregrounded and STOPPED means backgrounded. If this + * assumption is false, the logic in this class may be incorrect. + */ +class StartupStateProvider( + private val startupLog: StartupActivityLog, + private val startReasonProvider: AppStartReasonProvider +) { + + /** + * The restoration state of the application upon this most recent start up. See the + * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + * for specific definitions. + */ + enum class StartupState { + COLD, WARM, HOT, + + /** + * A start up state where we weren't able to bucket it into the other categories. + * This includes, but is not limited to: + * - if the activity this is called from is not currently started + * - if the currently started activity is not the first started activity + */ + UNKNOWN; + } + + /** + * Returns the [StartupState] for the currently started activity. Note: the state will be + * [StartupState.UNKNOWN] if the currently started activity is not the first started activity. + * + * This method must be called after the foreground Activity is STARTED. + */ + fun getStartupStateForStartedActivity(activityClass: Class): StartupState = when { + isColdStartForStartedActivity(activityClass) -> StartupState.COLD + isWarmStartForStartedActivity(activityClass) -> StartupState.WARM + isHotStartForStartedActivity(activityClass) -> StartupState.HOT + else -> StartupState.UNKNOWN + } + + /** + * Returns true if the current startup state is COLD and the currently started activity is the + * first started activity (i.e. we can use it for performance measurements). + * + * This method must be called after the foreground Activity is STARTED. + */ + fun isColdStartForStartedActivity(activityClass: Class): Boolean { + // A cold start means: + // - the process was started for the first started activity (e.g. not a service) + // - the first started activity ever is still active + // + // Thus, for the activity log we expect: + // [... Activity-STARTED, App-STARTED] + // since if another Activity was started, it would appear after App-STARTED. This is where: + // - the app has not been stopped ever + if (startReasonProvider.reason != StartReason.ACTIVITY) { + return false + } + + val isLastStartedActivityStillStarted = startupLog.log.takeLast(2) == listOf( + LogEntry.ActivityStarted(activityClass), + LogEntry.AppStarted + ) + return !startupLog.log.contains(LogEntry.AppStopped) && isLastStartedActivityStillStarted + } + + /** + * A short-circuit implementation of [isColdStartForStartedActivity] that will return false early + * so we don't have to call [isColdStartForStartedActivity]. + * + * When this can be called might be tightly coupled to [ColdStartupDurationTelemetry]: use at + * your own risk. + */ + fun shouldShortCircuitColdStart(): Boolean = startupLog.log.contains(LogEntry.AppStopped) + + /** + * Returns true if the current startup state is WARM and the currently started activity is the + * first started activity for this start up (i.e. we can use it for performance measurements). + * + * This method must be called after the foreground activity is STARTED. + */ + fun isWarmStartForStartedActivity(activityClass: Class): Boolean { + // A warm start means: + // - the app was backgrounded and has since been started + // - the first started activity since the app was started is still active. + // - that activity was created before being started + // + // For the activity log, we expect: + // [... App-STOPPED, ... Activity-CREATED, Activity-STARTED, App-STARTED] + // where: + // - App-STOPPED is the last STOPPED seen + // - we're assuming App-STARTED will only be last if one activity is started (as observed) + if (!startupLog.log.contains(LogEntry.AppStopped)) { + return false // if the app hasn't been stopped, it's not a warm start. + } + val afterLastStopped = startupLog.log.takeLastWhile { it != LogEntry.AppStopped } + + @Suppress("MagicNumber") // we take a specific number at the end of the list to compare them. + val isLastActivityCreatedStillStarted = afterLastStopped.takeLast(3) == listOf( + LogEntry.ActivityCreated(activityClass), + LogEntry.ActivityStarted(activityClass), + LogEntry.AppStarted + ) + return isLastActivityCreatedStillStarted + } + + /** + * Returns true if the current startup state is HOT and the currently started activity is the + * first started activity for this start up (i.e. we can use it for performance measurements). + * + * This method must be called after the foreground activity is STARTED. + */ + fun isHotStartForStartedActivity(activityClass: Class): Boolean { + // A hot start means: + // - the app was backgrounded and has since been started + // - the first started activity since the app was started is still active. + // - that activity was not created before being started + // + // For the activity log, we expect: + // [... App-STOPPED, ... Activity-STARTED, App-STARTED] + // where: + // - App-STOPPED is the last STOPPED seen + // - App-CREATED is NOT called for this activity + // - we're assuming App-STARTED will only be last if one activity is started (as observed) + if (!startupLog.log.contains(LogEntry.AppStopped)) { + return false // if the app hasn't been stopped, it's not a hot start. + } + val afterLastStopped = startupLog.log.takeLastWhile { it != LogEntry.AppStopped } + + val isLastActivityStartedStillStarted = afterLastStopped.takeLast(2) == listOf( + LogEntry.ActivityStarted(activityClass), + LogEntry.AppStarted + ) + return !afterLastStopped.contains(LogEntry.ActivityCreated(activityClass)) && + isLastActivityStartedStillStarted + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt index 2187d8e15..7fb511347 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupTimeline.kt @@ -40,7 +40,7 @@ object StartupTimeline { private var state: StartupState = StartupState.Cold(StartupDestination.UNKNOWN) private val reportFullyDrawn by lazy { StartupReportFullyDrawn() } - private val frameworkStartMeasurement by lazy { StartupFrameworkStartMeasurement() } + internal val frameworkStartMeasurement by lazy { StartupFrameworkStartMeasurement() } internal val homeActivityLifecycleObserver by lazy { StartupHomeActivityLifecycleObserver(frameworkStartMeasurement) } diff --git a/app/src/main/java/org/mozilla/fenix/perf/StartupTypeTelemetry.kt b/app/src/main/java/org/mozilla/fenix/perf/StartupTypeTelemetry.kt new file mode 100644 index 000000000..07b07855a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupTypeTelemetry.kt @@ -0,0 +1,96 @@ +/* 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 androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.NONE +import androidx.annotation.VisibleForTesting.PRIVATE +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.GleanMetrics.PerfStartup +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.perf.StartupPathProvider.StartupPath +import org.mozilla.fenix.perf.StartupStateProvider.StartupState + +private val activityClass = HomeActivity::class.java + +private val logger = Logger("StartupTypeTelemetry") + +/** + * Records telemetry for the number of start ups. See the + * [Fenix perf glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary) + * for specific definitions. + * + * This should be a member variable of [HomeActivity] because its data is tied to the lifecycle of an + * Activity. Call [attachOnHomeActivityOnCreate] for this class to work correctly. + * + * N.B.: this class is lightly hardcoded to HomeActivity. + */ +class StartupTypeTelemetry( + private val startupStateProvider: StartupStateProvider, + private val startupPathProvider: StartupPathProvider +) { + + fun attachOnHomeActivityOnCreate(lifecycle: Lifecycle) { + lifecycle.addObserver(StartupTypeLifecycleObserver()) + } + + private fun getTelemetryLabel(startupState: StartupState, startupPath: StartupPath): String { + // We don't use the enum name directly to avoid unintentional changes when refactoring. + val stateLabel = when (startupState) { + StartupState.COLD -> "cold" + StartupState.WARM -> "warm" + StartupState.HOT -> "hot" + StartupState.UNKNOWN -> "unknown" + } + + val pathLabel = when (startupPath) { + StartupPath.MAIN -> "main" + StartupPath.VIEW -> "view" + + // To avoid combinatorial explosion in label names, we bucket NOT_SET into UNKNOWN. + StartupPath.NOT_SET, + StartupPath.UNKNOWN -> "unknown" + } + + return "${stateLabel}_$pathLabel" + } + + @VisibleForTesting(otherwise = NONE) + fun getTestCallbacks() = StartupTypeLifecycleObserver() + + @VisibleForTesting(otherwise = PRIVATE) + fun record() { + val startupState = startupStateProvider.getStartupStateForStartedActivity(activityClass) + val startupPath = startupPathProvider.startupPathForActivity + val label = getTelemetryLabel(startupState, startupPath) + + PerfStartup.startupType[label].add(1) + logger.info("Recorded start up: $label") + } + + @VisibleForTesting(otherwise = PRIVATE) + inner class StartupTypeLifecycleObserver : DefaultLifecycleObserver { + private var shouldRecordStart = false + + override fun onStart(owner: LifecycleOwner) { + shouldRecordStart = true + } + + override fun onResume(owner: LifecycleOwner) { + // We must record in onResume because the StartupStateProvider can only be called for + // STARTED activities and we can't guarantee our onStart is called before its. + // + // We only record if start was called for this resume to avoid recording + // for onPause -> onResume states. + if (shouldRecordStart) { + record() + shouldRecordStart = false + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt b/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt index 845076d8c..0878a2651 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt @@ -95,21 +95,7 @@ class StrictModeManager( * specific fragment. */ fun attachListenerToDisablePenaltyDeath(fragmentManager: FragmentManager) { - fragmentManager.registerFragmentLifecycleCallbacks(object : - FragmentManager.FragmentLifecycleCallbacks() { - override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { - fm.unregisterFragmentLifecycleCallbacks(this) - - // If we don't post when using penaltyListener on P+, the violation listener is never - // called. My best guess is that, unlike penaltyDeath, the violations are not - // delivered instantaneously so posting gives time for the violation listeners to - // run before they are removed here. This may be a race so we give the listeners a - // little extra time to run too though this way we may accidentally trigger - // violations for non-startup, which we haven't planned to do yet. - Handler(mainLooper).postDelayed({ - enableStrictMode(setPenaltyDeath = false) - }, DELAY_TO_REMOVE_STRICT_MODE_MILLIS) - } }, false) + fragmentManager.registerFragmentLifecycleCallbacks(DisableStrictModeFragmentLifecycleCallbacks(), false) } /** @@ -147,6 +133,26 @@ class StrictModeManager( functionBlock() } } + + // If we use anonymous classes/functions in this class, we get a class load error with a slight perf impact. #18731 + inner class DisableStrictModeFragmentLifecycleCallbacks : FragmentManager.FragmentLifecycleCallbacks() { + override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { + fm.unregisterFragmentLifecycleCallbacks(this) + + // If we don't post when using penaltyListener on P+, the violation listener is never + // called. My best guess is that, unlike penaltyDeath, the violations are not + // delivered instantaneously so posting gives time for the violation listeners to + // run before they are removed here. This may be a race so we give the listeners a + // little extra time to run too though this way we may accidentally trigger + // violations for non-startup, which we haven't planned to do yet. + Handler(Looper.getMainLooper()).postDelayed(::disableStrictMode, DELAY_TO_REMOVE_STRICT_MODE_MILLIS) + } + + // See comment on anonymous functions above. + private fun disableStrictMode() { + enableStrictMode(setPenaltyDeath = false) + } + } } /** diff --git a/app/src/main/java/org/mozilla/fenix/push/FirebasePushService.kt b/app/src/main/java/org/mozilla/fenix/push/FirebasePushService.kt index 7e03039f4..43c349491 100644 --- a/app/src/main/java/org/mozilla/fenix/push/FirebasePushService.kt +++ b/app/src/main/java/org/mozilla/fenix/push/FirebasePushService.kt @@ -5,64 +5,12 @@ package org.mozilla.fenix.push import android.annotation.SuppressLint -import com.google.firebase.messaging.RemoteMessage -import com.google.firebase.messaging.FirebaseMessagingService -import com.leanplum.LeanplumPushFirebaseMessagingService -import com.leanplum.LeanplumPushService -import mozilla.components.concept.push.PushService -import mozilla.components.lib.push.firebase.AbstractFirebasePushService import mozilla.components.feature.push.AutoPushFeature - -/** - * A wrapper class that only exists to delegate to [FirebaseMessagingService] instances. - * - * Implementation notes: - * - * This was a doozy... - * - * With Firebase Cloud Messaging, we've been given some tight constraints in order to get this to - * work: - * - We want to have multiple FCM message receivers for AutoPush and LeanPlum (for now), however - * there can only be one registered [FirebaseMessagingService] in the AndroidManifest. - * - The [LeanplumPushFirebaseMessagingService] does not function as expected unless it's the - * inherited service that receives the messages. - * - The [AutoPushService] is not strongly tied to being the inherited service, but the - * [AutoPushFeature] requires a reference to the push instance as a [PushService]. - * - * We tried creating an empty [FirebaseMessagingService] that can hold a list of the services - * for delegating, but the [LeanplumPushFirebaseMessagingService] tries to get a reference to the - * Application Context, however,since the FCM service runs in a background process that gives a - * nullptr. Within LeanPlum, this is something that is probably provided internally. - * - * We tried to pass in an instance of the [AbstractFirebasePushService] to [FirebasePushService] - * through the constructor and delegate the implementation of a [PushService] to that, but alas, - * the service requires you to have an empty default constructor in order for the OS to do the - * initialization. For this reason, we created a singleton instance of the AutoPush instance since - * that lets us easily delegate the implementation to that, as well as make invocations when FCM - * receives new messages. - */ -class FirebasePushService : LeanplumPushFirebaseMessagingService(), - PushService by AutoPushService { - - override fun onCreate() { - LeanplumPushService.setCustomizer(LeanplumNotificationCustomizer()) - super.onCreate() - } - - override fun onNewToken(newToken: String) { - AutoPushService.onNewToken(newToken) - super.onNewToken(newToken) - } - - override fun onMessageReceived(remoteMessage: RemoteMessage) { - AutoPushService.onMessageReceived(remoteMessage) - super.onMessageReceived(remoteMessage) - } -} +import mozilla.components.lib.push.firebase.AbstractFirebasePushService /** * A singleton instance of the FirebasePushService needed for communicating between FCM and the * [AutoPushFeature]. */ @SuppressLint("MissingFirebaseInstanceTokenRefresh") // Implemented internally. -object AutoPushService : AbstractFirebasePushService() +class FirebasePushService : AbstractFirebasePushService() diff --git a/app/src/main/java/org/mozilla/fenix/push/LeanplumNotificationCustomizer.kt b/app/src/main/java/org/mozilla/fenix/push/LeanplumNotificationCustomizer.kt deleted file mode 100644 index 476595151..000000000 --- a/app/src/main/java/org/mozilla/fenix/push/LeanplumNotificationCustomizer.kt +++ /dev/null @@ -1,31 +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.push - -import android.app.Notification -import android.os.Bundle -import androidx.core.app.NotificationCompat -import com.leanplum.LeanplumPushNotificationCustomizer -import org.mozilla.fenix.R - -/** - * Notification customizer for incoming Leanplum push messages. - */ -class LeanplumNotificationCustomizer : LeanplumPushNotificationCustomizer { - override fun customize( - builder: NotificationCompat.Builder, - notificationPayload: Bundle? - ) { - builder.setSmallIcon(R.drawable.ic_status_logo) - } - - // Do not implement if unless we want to support 2 lines of text in the BigPicture style. - // See: https://docs.leanplum.com/docs/customize-your-push-notifications-sample-android - override fun customize( - builder: Notification.Builder?, - notificationPayload: Bundle?, - notificationStyle: Notification.Style? - ) = Unit // no-op -} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt index 4a4fe9c4f..a3484f633 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt @@ -24,6 +24,7 @@ import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricsUtils import org.mozilla.fenix.crashes.CrashListActivity import org.mozilla.fenix.ext.navigateSafe +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.utils.Settings @@ -32,7 +33,7 @@ import org.mozilla.fenix.utils.Settings */ @Suppress("TooManyFunctions") interface SearchController { - fun handleUrlCommitted(url: String) + fun handleUrlCommitted(url: String, fromHomeScreen: Boolean = false) fun handleEditingCancelled() fun handleTextChanged(text: String) fun handleUrlTapped(url: String) @@ -60,7 +61,7 @@ class SearchDialogController( private val clearToolbar: () -> Unit ) : SearchController { - override fun handleUrlCommitted(url: String) { + override fun handleUrlCommitted(url: String, fromHomeScreen: Boolean) { when (url) { "about:crashes" -> { // The list of past crashes can be accessed via "settings > about", but desktop and @@ -73,16 +74,19 @@ class SearchDialogController( SearchDialogFragmentDirections.actionGlobalAddonsManagementFragment() navController.navigateSafe(R.id.searchDialogFragment, directions) } - "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO)) + "moz://a" -> openSearchOrUrl( + SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO), + fromHomeScreen + ) else -> if (url.isNotBlank()) { - openSearchOrUrl(url) + openSearchOrUrl(url, fromHomeScreen) } } dismissDialog() } - private fun openSearchOrUrl(url: String) { + private fun openSearchOrUrl(url: String, fromHomeScreen: Boolean) { clearToolbarFocus() val searchEngine = fragmentStore.state.searchEngineSource.searchEngine @@ -91,7 +95,8 @@ class SearchDialogController( searchTermOrURL = url, newTab = fragmentStore.state.tabId == null, from = BrowserDirection.FromSearchDialog, - engine = searchEngine + engine = searchEngine, + requestDesktopMode = fromHomeScreen && activity.settings().openNextTabInDesktopMode ) val event = if (url.isUrl() || searchEngine == null) { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index 780724462..2f1296adf 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -168,20 +168,22 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { ) ) + val fromHomeFragment = + findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment + toolbarView = ToolbarView( requireContext(), interactor, historyStorageProvider(), isPrivate, view.toolbar, - requireComponents.core.engine + requireComponents.core.engine, + fromHomeFragment ) val awesomeBar = view.awesome_bar awesomeBar.customizeForBottomToolbar = requireContext().settings().shouldUseBottomToolbar - val fromHomeFragment = - findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment awesomeBarView = AwesomeBarView( activity, interactor, @@ -270,6 +272,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } fill_link_from_clipboard.setOnClickListener { + requireComponents.analytics.metrics.track(Event.ClipboardSuggestionClicked) view.hideKeyboard() toolbarView.view.clearFocus() (activity as HomeActivity) @@ -431,12 +434,11 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { dialog.cancel() } setPositiveButton(R.string.qr_scanner_dialog_positive) { dialog: DialogInterface, _ -> - (activity as HomeActivity) - .openToBrowserAndLoad( - searchTermOrURL = result, - newTab = store.state.tabId == null, - from = BrowserDirection.FromSearchDialog - ) + (activity as? HomeActivity)?.openToBrowserAndLoad( + searchTermOrURL = result, + newTab = store.state.tabId == null, + from = BrowserDirection.FromSearchDialog + ) dialog.dismiss() } create() diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt index 93d1068ba..23ab51129 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt @@ -17,8 +17,8 @@ class SearchDialogInteractor( private val searchController: SearchDialogController ) : AwesomeBarInteractor, ToolbarInteractor { - override fun onUrlCommitted(url: String) { - searchController.handleUrlCommitted(url) + override fun onUrlCommitted(url: String, fromHomeScreen: Boolean) { + searchController.handleUrlCommitted(url, fromHomeScreen) } override fun onEditingCanceled() { diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index da4cc0612..dee2033e5 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -159,7 +159,11 @@ class AwesomeBarView( icon = searchBitmap, showDescription = false, engine = engineForSpeculativeConnects, - filterExactMatch = true + filterExactMatch = true, + private = when (activity.browsingModeManager.mode) { + BrowsingMode.Normal -> false + BrowsingMode.Private -> true + } ) defaultSearchActionProvider = @@ -343,7 +347,11 @@ class AwesomeBarView( mode = SearchSuggestionProvider.Mode.MULTIPLE_SUGGESTIONS, icon = searchBitmap, engine = engineForSpeculativeConnects, - filterExactMatch = true + filterExactMatch = true, + private = when (activity.browsingModeManager.mode) { + BrowsingMode.Normal -> false + BrowsingMode.Private -> true + } ) ) } diff --git a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt index dea282f36..1462be820 100644 --- a/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/toolbar/ToolbarView.kt @@ -31,8 +31,9 @@ interface ToolbarInteractor { /** * Called when a user hits the return key while [ToolbarView] has focus. * @param url the text inside the [ToolbarView] when committed + * @param fromHomeScreen true if the toolbar has been opened from home screen */ - fun onUrlCommitted(url: String) + fun onUrlCommitted(url: String, fromHomeScreen: Boolean = false) /** * Called when a user removes focus from the [ToolbarView] @@ -49,13 +50,15 @@ interface ToolbarInteractor { /** * View that contains and configures the BrowserToolbar to only be used in its editing mode. */ +@Suppress("LongParameterList") class ToolbarView( private val context: Context, private val interactor: ToolbarInteractor, private val historyStorage: HistoryStorage?, private val isPrivate: Boolean, val view: BrowserToolbar, - engine: Engine + engine: Engine, + fromHomeFragment: Boolean ) { @VisibleForTesting @@ -70,7 +73,7 @@ class ToolbarView( // from resizing in case the BrowserFragment is being displayed before the // keyboard is gone: https://github.com/mozilla-mobile/fenix/issues/8399 hideKeyboard() - interactor.onUrlCommitted(it) + interactor.onUrlCommitted(it, fromHomeFragment) false } diff --git a/app/src/main/java/org/mozilla/fenix/selection/SelectionHolder.kt b/app/src/main/java/org/mozilla/fenix/selection/SelectionHolder.kt new file mode 100644 index 000000000..fe2b40d73 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/selection/SelectionHolder.kt @@ -0,0 +1,12 @@ +/* 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.selection + +/** + * Contains the selection of items added or removed using the [SelectionInteractor]. + */ +interface SelectionHolder { + val selectedItems: Set +} diff --git a/app/src/main/java/org/mozilla/fenix/selection/SelectionInteractor.kt b/app/src/main/java/org/mozilla/fenix/selection/SelectionInteractor.kt new file mode 100644 index 000000000..320bf1e04 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/selection/SelectionInteractor.kt @@ -0,0 +1,29 @@ +/* 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.selection + +/** + * Interactor for items that can be selected on the bookmarks and history screens. + */ +interface SelectionInteractor { + /** + * Called when an item is tapped to open it. + * @param item the tapped item to open. + */ + fun open(item: T) + + /** + * Called when an item is long pressed and selection mode is started, + * or when selection mode has already started an an item is tapped. + * @param item the item to select. + */ + fun select(item: T) + + /** + * Called when a selected item is tapped in selection mode and should no longer be selected. + * @param item the item to deselect. + */ + fun deselect(item: T) +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt index ca0229a2a..eb0c11332 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt @@ -67,13 +67,6 @@ class DataChoicesFragment : PreferenceFragmentCompat() { requirePreference(R.string.pref_key_marketing_telemetry).apply { isChecked = context.settings().isMarketingTelemetryEnabled - - val appName = context.getString(R.string.app_name) - summary = String.format( - context.getString(R.string.preferences_marketing_data_description), - appName - ) - onPreferenceChangeListener = SharedPreferenceUpdater() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt index cc398781d..e07083985 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PairFragment.kt @@ -13,13 +13,13 @@ import android.view.View import androidx.core.content.ContextCompat import androidx.core.content.getSystemService import androidx.fragment.app.Fragment -import androidx.navigation.fragment.NavHostFragment.findNavController import androidx.navigation.fragment.findNavController import mozilla.components.feature.qr.QrFeature import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.R import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler { @@ -67,7 +67,11 @@ class PairFragment : Fragment(R.layout.fragment_pair), UserInteractionHandler { false ) }, - scanMessage = R.string.pair_instructions_2 + scanMessage = + if (requireContext().settings().allowDomesticChinaFxaServer && + org.mozilla.fenix.Config.channel.isMozillaOnline) + R.string.pair_instructions_2_cn + else R.string.pair_instructions_2 ), owner = this, view = view diff --git a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt index e46b6ae52..dc1579f74 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt @@ -48,7 +48,7 @@ enum class PhoneFeature(val androidPermissionsList: Array) : Parcelable @StringRes val stringRes = if (isAndroidPermissionGranted(context)) { when (this) { AUTOPLAY_AUDIBLE -> - when (settings?.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) ?: AUTOPLAY_BLOCK_ALL) { + when (settings?.getAutoplayUserSetting() ?: AUTOPLAY_BLOCK_ALL) { AUTOPLAY_ALLOW_ALL -> R.string.preference_option_autoplay_allowed2 AUTOPLAY_ALLOW_ON_WIFI -> R.string.preference_option_autoplay_allowed_wifi_only2 AUTOPLAY_BLOCK_AUDIBLE -> R.string.preference_option_autoplay_block_audio2 diff --git a/app/src/main/java/org/mozilla/fenix/settings/SecretDebugSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SecretDebugSettingsFragment.kt index bedc939e6..9ac930728 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SecretDebugSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SecretDebugSettingsFragment.kt @@ -7,7 +7,6 @@ package org.mozilla.fenix.settings import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import com.leanplum.Leanplum import org.mozilla.fenix.R import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.showToolbar @@ -24,26 +23,6 @@ class SecretDebugSettingsFragment : PreferenceFragmentCompat() { val store = requireComponents.core.store - requirePreference(R.string.pref_key_leanplum_user_id).apply { - summary = Leanplum.getUserId().let { - if (it.isNullOrEmpty()) { - "No User Id" - } else { - it - } - } - } - - requirePreference(R.string.pref_key_leanplum_device_id).apply { - summary = Leanplum.getDeviceId().let { - if (it.isNullOrEmpty()) { - "No Device Id" - } else { - it - } - } - } - requirePreference(R.string.pref_key_search_region_home).apply { summary = store.state.search.region?.home ?: "Unknown" } 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 161e45295..518bc3f4f 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -6,7 +6,6 @@ package org.mozilla.fenix.settings import android.annotation.SuppressLint import android.app.Activity -import android.app.role.RoleManager import android.content.ActivityNotFoundException import android.content.DialogInterface import android.content.Intent @@ -25,6 +24,7 @@ import androidx.navigation.findNavController import androidx.navigation.fragment.navArgs import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat +import androidx.preference.SwitchPreference import androidx.recyclerview.widget.RecyclerView import kotlinx.android.synthetic.main.amo_collection_override_dialog.view.* import kotlinx.coroutines.CoroutineScope @@ -34,23 +34,28 @@ 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.getColorFromAttr import mozilla.components.support.ktx.android.view.showKeyboard import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.experiments.ExperimentBranch +import org.mozilla.fenix.experiments.Experiments import org.mozilla.fenix.ext.application import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph 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.REQUEST_CODE_BROWSER_ROLE +import org.mozilla.fenix.ext.openSetDefaultBrowserOption import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.ext.withExperiment import org.mozilla.fenix.settings.account.AccountUiView +import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.Settings import kotlin.system.exitProcess @@ -91,7 +96,8 @@ class SettingsFragment : PreferenceFragmentCompat() { scope = lifecycleScope, accountManager = requireComponents.backgroundServices.accountManager, httpClient = requireComponents.core.client, - updateFxASyncOverrideMenu = ::updateFxASyncOverrideMenu + updateFxASyncOverrideMenu = ::updateFxASyncOverrideMenu, + updateFxAAllowDomesticChinaServerMenu = :: updateFxAAllowDomesticChinaServerMenu ) // Observe account changes to keep the UI up-to-date. @@ -133,15 +139,23 @@ class SettingsFragment : PreferenceFragmentCompat() { } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - val preferencesId = if (FeatureFlags.newIconSet) { - R.xml.preferences_without_icons - } else { - R.xml.preferences - } + val preferencesId = getPreferenceLayoutId() + setPreferencesFromResource(preferencesId, rootKey) updateMakeDefaultBrowserPreference() } + /** + * @return The preference layout to be used depending on flags and existing experiment branches. + * Note: Changing Settings screen before experiment is over requires changing all layouts. + */ + private fun getPreferenceLayoutId() = + if (isDefaultBrowserExperimentBranch() && !isFirefoxDefaultBrowser()) { + R.xml.preferences_default_browser_experiment + } else { + R.xml.preferences + } + @SuppressLint("RestrictedApi") override fun onResume() { super.onResume() @@ -276,6 +290,9 @@ class SettingsFragment : PreferenceFragmentCompat() { resources.getString(R.string.pref_key_passwords) -> { SettingsFragmentDirections.actionSettingsFragmentToSavedLoginsAuthFragment() } + resources.getString(R.string.pref_key_credit_cards) -> { + SettingsFragmentDirections.actionSettingsFragmentToCreditCardsSettingFragment() + } resources.getString(R.string.pref_key_about) -> { SettingsFragmentDirections.actionSettingsFragmentToAboutFragment() } @@ -321,6 +338,9 @@ class SettingsFragment : PreferenceFragmentCompat() { resources.getString(R.string.pref_key_secret_debug_info) -> { SettingsFragmentDirections.actionSettingsFragmentToSecretInfoSettingsFragment() } + resources.getString(R.string.pref_key_nimbus_experiments) -> { + SettingsFragmentDirections.actionSettingsFragmentToNimbusExperimentsFragment() + } resources.getString(R.string.pref_key_override_amo_collection) -> { val context = requireContext() val dialogView = LayoutInflater.from(context).inflate(R.layout.amo_collection_override_dialog, null) @@ -365,8 +385,6 @@ class SettingsFragment : PreferenceFragmentCompat() { private fun setupPreferences() { val leakKey = getPreferenceKey(R.string.pref_key_leakcanary) val debuggingKey = getPreferenceKey(R.string.pref_key_remote_debugging) - val preferencePrivateBrowsing = - requirePreference(R.string.pref_key_private_browsing) val preferenceLeakCanary = findPreference(leakKey) val preferenceRemoteDebugging = findPreference(debuggingKey) val preferenceMakeDefaultBrowser = @@ -374,12 +392,6 @@ class SettingsFragment : PreferenceFragmentCompat() { val preferenceOpenLinksInExternalApp = findPreference(getPreferenceKey(R.string.pref_key_open_links_in_external_app)) - if (!FeatureFlags.newIconSet) { - preferencePrivateBrowsing.icon.mutate().apply { - setTint(requireContext().getColorFromAttr(R.attr.primaryText)) - } - } - if (!Config.channel.isReleased) { preferenceLeakCanary?.setOnPreferenceChangeListener { _, newValue -> val isEnabled = newValue == true @@ -423,14 +435,24 @@ class SettingsFragment : PreferenceFragmentCompat() { } preferenceFxAOverride?.onPreferenceChangeListener = syncFxAOverrideUpdater preferenceSyncOverride?.onPreferenceChangeListener = syncFxAOverrideUpdater - findPreference( - getPreferenceKey(R.string.pref_key_debug_settings) - )?.isVisible = requireContext().settings().showSecretDebugMenuThisSession - findPreference( - getPreferenceKey(R.string.pref_key_secret_debug_info) - )?.isVisible = requireContext().settings().showSecretDebugMenuThisSession + + with(requireContext().settings()) { + findPreference( + getPreferenceKey(R.string.pref_key_credit_cards) + )?.isVisible = creditCardsFeature + findPreference( + getPreferenceKey(R.string.pref_key_nimbus_experiments) + )?.isVisible = showSecretDebugMenuThisSession + findPreference( + getPreferenceKey(R.string.pref_key_debug_settings) + )?.isVisible = showSecretDebugMenuThisSession + findPreference( + getPreferenceKey(R.string.pref_key_secret_debug_info) + )?.isVisible = showSecretDebugMenuThisSession + } setupAmoCollectionOverridePreference(requireContext().settings()) + setupAllowDomesticChinaFxaServerPreference() } /** @@ -439,44 +461,12 @@ class SettingsFragment : PreferenceFragmentCompat() { * For Open sumo page to show user how to change default app. */ private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener { - return when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { - Preference.OnPreferenceClickListener { - requireContext().getSystemService(RoleManager::class.java).also { - if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld( - RoleManager.ROLE_BROWSER - ) - ) { - startActivityForResult( - it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), - REQUEST_CODE_BROWSER_ROLE - ) - } else { - navigateUserToDefaultAppsSettings() - } - } - true - } - } - Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> { - Preference.OnPreferenceClickListener { - navigateUserToDefaultAppsSettings() - true - } - } - else -> { - Preference.OnPreferenceClickListener { - (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getSumoURLForTopic( - requireContext(), - SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER - ), - newTab = true, - from = BrowserDirection.FromSettings - ) - true - } + return Preference.OnPreferenceClickListener { + if (isDefaultBrowserExperimentBranch() && !isFirefoxDefaultBrowser()) { + requireContext().metrics.track(Event.SetDefaultBrowserSettingsScreenClicked) } + activity?.openSetDefaultBrowserOption() + true } } @@ -489,21 +479,16 @@ class SettingsFragment : PreferenceFragmentCompat() { } } - private fun navigateUserToDefaultAppsSettings() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - val intent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) - startActivity(intent) - } - } - private fun updateMakeDefaultBrowserPreference() { - requirePreference(R.string.pref_key_make_default_browser).updateSwitch() + if (!isDefaultBrowserExperimentBranch()) { + requirePreference(R.string.pref_key_make_default_browser).updateSwitch() + } } private fun navigateFromSettings(directions: NavDirections) { view?.findNavController()?.let { navController -> if (navController.currentDestination?.id == R.id.settingsFragment) { - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } } } @@ -520,6 +505,22 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + private fun updateFxAAllowDomesticChinaServerMenu() { + val settings = requireContext().settings() + val preferenceAllowDomesticChinaServer = + findPreference(getPreferenceKey(R.string.pref_key_allow_domestic_china_fxa_server)) + // Only enable changes to these prefs when the user isn't connected to an account. + val enabled = + requireComponents.backgroundServices.accountManager.authenticatedAccount() == null + val checked = settings.allowDomesticChinaFxaServer + val visible = Config.channel.isMozillaOnline + preferenceAllowDomesticChinaServer?.apply { + isEnabled = enabled + isChecked = checked + isVisible = visible + } + } + private fun updateFxASyncOverrideMenu() { val preferenceFxAOverride = findPreference(getPreferenceKey(R.string.pref_key_override_fxa_server)) @@ -558,8 +559,46 @@ class SettingsFragment : PreferenceFragmentCompat() { } } + private fun setupAllowDomesticChinaFxaServerPreference() { + val allowDomesticChinaFxAServer = getPreferenceKey(R.string.pref_key_allow_domestic_china_fxa_server) + val preferenceAllowDomesticChinaFxAServer = findPreference(allowDomesticChinaFxAServer) + val visible = Config.channel.isMozillaOnline + + preferenceAllowDomesticChinaFxAServer?.apply { + isVisible = visible + } + + if (visible) { + preferenceAllowDomesticChinaFxAServer?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { preference, newValue -> + preference.context.settings().preferences.edit() + .putBoolean(preference.key, newValue as Boolean).apply() + updateFxAAllowDomesticChinaServerMenu() + Toast.makeText( + context, + getString(R.string.toast_override_fxa_sync_server_done), + Toast.LENGTH_LONG + ).show() + Handler(Looper.getMainLooper()).postDelayed({ + exitProcess(0) + }, FXA_SYNC_OVERRIDE_EXIT_DELAY) + } + } + } + + private fun isDefaultBrowserExperimentBranch(): Boolean { + val experiments = context?.components?.analytics?.experiments + return experiments?.withExperiment(Experiments.DEFAULT_BROWSER) { experimentBranch -> + (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_SETTINGS_MENU) + } == true + } + + private fun isFirefoxDefaultBrowser(): Boolean { + val browsers = BrowsersCache.all(requireContext()) + return browsers.isFirefoxDefaultBrowser + } + companion object { - private const val REQUEST_CODE_BROWSER_ROLE = 1 private const val SCROLL_INDICATOR_DELAY = 10L private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L 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 0c356cc84..bcded9406 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -33,6 +33,8 @@ object SupportUtils { "?e=&p=AyIGZRprFDJWWA1FBCVbV0IUWVALHFRBEwQAQB1AWQkFVUVXfFkAF14lRFRbJXstVWR3WQ1rJ08AZnhS" + "HDJBYh4LZR9eEAMUBlccWCUBEQZRGFoXCxc3ZRteJUl8BmUZWhQ" + "AEwdRGF0cMhIAVB5ZFAETBVAaXRwyFQdcKydLSUpaCEtYFAIXN2UrWCUyIgdVK1slXVZaCCtZFAMWDg%3D%3D" + const val PDD_URL = "https://mobile.yangkeduo.com/duo_cms_mall.html?pid=13289095_194240604&" + + "cpsSign=CM_210309_13289095_194240604_8bcfd56d5db3c43d983014d2658ec26e&duoduo_type=2" const val GOOGLE_US_URL = "https://www.google.com/webhp?client=firefox-b-1-m&channel=ts" const val GOOGLE_XX_URL = "https://www.google.com/webhp?client=firefox-b-m&channel=ts" diff --git a/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt index 66bb347cc..92be8a332 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt @@ -16,6 +16,7 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -32,7 +33,7 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() { private val exceptionsClickListener = Preference.OnPreferenceClickListener { val directions = TrackingProtectionFragmentDirections.actionTrackingProtectionFragmentToExceptionsFragment() - requireView().findNavController().navigate(directions) + requireView().findNavController().navigateBlockingForAsyncNavGraph(directions) true } private lateinit var customCookies: CheckBoxPreference 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 9f80cf5e6..ba1d7a886 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 @@ -22,6 +22,7 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.crashes.CrashListActivity +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.settings.SupportUtils @@ -178,7 +179,7 @@ class AboutFragment : Fragment(), AboutPageListener { private fun openLibrariesPage() { val navController = findNavController() - navController.navigate(R.id.action_aboutFragment_to_aboutLibrariesFragment) + navController.navigateBlockingForAsyncNavGraph(R.id.action_aboutFragment_to_aboutLibrariesFragment) } override fun onAboutItemClicked(item: AboutItem) { diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt index 965ff46cf..94464b035 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AccountUiView.kt @@ -26,7 +26,8 @@ class AccountUiView( private val scope: CoroutineScope, private val accountManager: FxaAccountManager, private val httpClient: Client, - private val updateFxASyncOverrideMenu: () -> Unit + private val updateFxASyncOverrideMenu: () -> Unit, + private val updateFxAAllowDomesticChinaServerMenu: () -> Unit ) { private val preferenceSignIn = @@ -48,6 +49,7 @@ class AccountUiView( val account = accountManager.authenticatedAccount() updateFxASyncOverrideMenu() + updateFxAAllowDomesticChinaServerMenu() // Signed-in, no problems. if (account != null && !accountManager.accountNeedsReauth()) { diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt index fe0b30c2a..e5f960008 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt @@ -66,9 +66,10 @@ class SignOutFragment : AppCompatDialogFragment() { accountManager.logout() }.invokeOnCompletion { runIfFragmentIsAttached { - if (!findNavController().popBackStack(R.id.settingsFragment, false)) { + if (this.isVisible) { dismiss() } + findNavController().popBackStack() } } } 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 27734d945..2433bb5b2 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 @@ -21,10 +21,12 @@ import mozilla.components.concept.sync.OAuthAccount import mozilla.components.support.ktx.android.content.hasCamera import mozilla.components.support.ktx.android.content.isPermissionGranted import mozilla.components.support.ktx.android.view.hideKeyboard +import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -58,7 +60,7 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { private fun navigateToPairFragment() { val directions = TurnOnSyncFragmentDirections.actionTurnOnSyncFragmentToPairFragment() - requireView().findNavController().navigate(directions) + requireView().findNavController().navigateBlockingForAsyncNavGraph(directions) requireComponents.analytics.metrics.track(Event.SyncAuthScanPairing) } @@ -114,7 +116,9 @@ class TurnOnSyncFragment : Fragment(), AccountObserver { view.signInScanButton.setOnClickListener(paringClickListener) view.signInEmailButton.setOnClickListener(signInClickListener) view.signInInstructions.text = HtmlCompat.fromHtml( - getString(R.string.sign_in_instructions), + if (requireContext().settings().allowDomesticChinaFxaServer && Config.channel.isMozillaOnline) + getString(R.string.sign_in_instructions_cn) + else getString(R.string.sign_in_instructions), HtmlCompat.FROM_HTML_MODE_LEGACY ) diff --git a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt index 2421d45d9..3163e9fe3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/advanced/LocaleViewHolders.kt @@ -23,10 +23,14 @@ class LocaleViewHolder( if (locale.toString().equals("vec", ignoreCase = true)) { locale.toString() } - // Capitalisation is done using the rules of the appropriate locale (endonym and exonym). - locale_title_text.text = getDisplayName(locale) - // Show the given locale using the device locale for the subtitle. - locale_subtitle_text.text = locale.getProperDisplayName() + if (locale.language == "zh") { + bindChineseLocale(locale) + } else { + // Capitalisation is done using the rules of the appropriate locale (endonym and exonym). + locale_title_text.text = getDisplayName(locale) + // Show the given locale using the device locale for the subtitle. + locale_subtitle_text.text = locale.getProperDisplayName() + } locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = false) itemView.setOnClickListener { @@ -34,6 +38,20 @@ class LocaleViewHolder( } } + private fun bindChineseLocale(locale: Locale) { + if (locale.country == "CN") { + locale_title_text.text = + Locale.forLanguageTag("zh-Hans").getDisplayName(locale).capitalize(locale) + locale_subtitle_text.text = + Locale.forLanguageTag("zh-Hans").displayName.capitalize(Locale.getDefault()) + } else if (locale.country == "TW") { + locale_title_text.text = + Locale.forLanguageTag("zh-Hant").getDisplayName(locale).capitalize(locale) + locale_subtitle_text.text = + Locale.forLanguageTag("zh-Hant").displayName.capitalize(Locale.getDefault()) + } + } + private fun getDisplayName(locale: Locale): String { val displayName = locale.getDisplayName(locale).capitalize(locale) if (displayName.equals(locale.toString(), ignoreCase = true)) { @@ -255,8 +273,16 @@ class SystemLocaleViewHolder( override fun bind(locale: Locale) { locale_title_text.text = itemView.context.getString(R.string.default_locale_text) - // Use the device locale for the system locale subtitle. - locale_subtitle_text.text = locale.getDisplayName(locale).capitalize(locale) + if (locale.script == "Hant") { + locale_subtitle_text.text = + Locale.forLanguageTag("zh-Hant").displayName.capitalize(Locale.getDefault()) + } else if (locale.script == "Hans") { + locale_subtitle_text.text = + Locale.forLanguageTag("zh-Hans").displayName.capitalize(Locale.getDefault()) + } else { + // Use the device locale for the system locale subtitle. + locale_subtitle_text.text = locale.getDisplayName(locale).capitalize(locale) + } locale_selected_icon.isVisible = isCurrentLocaleSelected(locale, isDefault = true) itemView.setOnClickListener { interactor.onDefaultLocaleSelected() diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt new file mode 100644 index 000000000..91b9ac8ce --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorFragment.kt @@ -0,0 +1,108 @@ +/* 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.settings.creditcards + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs +import kotlinx.android.synthetic.main.fragment_credit_card_editor.* +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.support.ktx.android.view.hideKeyboard +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardEditorController +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardEditorInteractor +import org.mozilla.fenix.settings.creditcards.interactor.DefaultCreditCardEditorInteractor +import org.mozilla.fenix.settings.creditcards.view.CreditCardEditorView + +/** + * Display a credit card editor for adding and editing a credit card. + */ +class CreditCardEditorFragment : Fragment(R.layout.fragment_credit_card_editor) { + + private val args by navArgs() + + /** + * Returns true if a credit card is being edited, and false otherwise. + */ + private val isEditing: Boolean + get() = args.creditCard != null + + private lateinit var interactor: CreditCardEditorInteractor + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setHasOptionsMenu(true) + + if (!isEditing) { + showToolbar(getString(R.string.credit_cards_add_card)) + } else { + showToolbar(getString(R.string.credit_cards_edit_card)) + } + + interactor = DefaultCreditCardEditorInteractor( + controller = DefaultCreditCardEditorController( + storage = requireContext().components.core.autofillStorage, + lifecycleScope = lifecycleScope, + navController = findNavController() + ) + ) + + val creditCardEditorState = + args.creditCard?.toCreditCardEditorState() ?: getInitialCreditCardEditorState() + CreditCardEditorView(view, interactor).bind(creditCardEditorState) + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.credit_card_editor, menu) + + menu.findItem(R.id.delete_credit_card_button).isVisible = isEditing + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { + R.id.delete_credit_card_button -> { + args.creditCard?.let { interactor.onDeleteCardButtonClicked(it.guid) } + true + } + R.id.save_credit_card_button -> { + view?.hideKeyboard() + + val creditCard = args.creditCard + val creditCardFields = UpdatableCreditCardFields( + billingName = name_on_card_input.text.toString(), + cardNumber = card_number_input.text.toString(), + expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(), + expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(), + cardType = CARD_TYPE_PLACEHOLDER + ) + + if (creditCard != null) { + interactor.onUpdateCreditCard(creditCard.guid, creditCardFields) + } else { + interactor.onSaveCreditCard(creditCardFields) + } + + true + } + else -> false + } + + companion object { + // Number of years to show in the expiry year dropdown. + const val NUMBER_OF_YEARS_TO_SHOW = 10 + + // Placeholder for the card type. This will be replaced when we can identify the card type. + // This is dependent on https://github.com/mozilla-mobile/android-components/issues/9813. + const val CARD_TYPE_PLACEHOLDER = "" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorState.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorState.kt new file mode 100644 index 000000000..ed9c62747 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardEditorState.kt @@ -0,0 +1,61 @@ +/* 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.settings.creditcards + +import mozilla.components.concept.storage.CreditCard +import org.mozilla.fenix.settings.creditcards.CreditCardEditorFragment.Companion.NUMBER_OF_YEARS_TO_SHOW +import java.util.Calendar + +/** + * The state for the [CreditCardEditorFragment]. + * + * @property guid The unique identifier for the edited credit card. + * @property billingName The credit card billing name to display. + * @property cardNumber The credit card number to display. + * @property expiryMonth The selected credit card expiry month. + * @property expiryYears The range of expiry years to display. + * @property isEditing Whether or not the credit card is being edited. + */ +data class CreditCardEditorState( + val guid: String = "", + val billingName: String = "", + val cardNumber: String = "", + val expiryMonth: Int = 1, + val expiryYears: Pair, + val isEditing: Boolean = false +) + +/** + * Returns a [CreditCardEditorState] from the given [CreditCard]. + */ +fun CreditCard.toCreditCardEditorState(): CreditCardEditorState { + val startYear = expiryYear.toInt() + val endYear = startYear + NUMBER_OF_YEARS_TO_SHOW + + return CreditCardEditorState( + guid = guid, + billingName = billingName, + cardNumber = cardNumber, + expiryMonth = expiryMonth.toInt(), + expiryYears = Pair(startYear, endYear), + isEditing = true + ) +} + +/** + * Returns the initial credit editor state if no credit card is provided. + * + * @return an empty [CreditCardEditorState] with a range of expiry years based on the latest + * 10 years. + */ +fun getInitialCreditCardEditorState(): CreditCardEditorState { + val calendar = Calendar.getInstance() + val startYear = calendar.get(Calendar.YEAR) + val endYear = startYear + NUMBER_OF_YEARS_TO_SHOW + + return CreditCardEditorState( + expiryYears = Pair(startYear, endYear) + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsFragmentStore.kt new file mode 100644 index 000000000..043607625 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsFragmentStore.kt @@ -0,0 +1,64 @@ +/* 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.settings.creditcards + +import mozilla.components.concept.storage.CreditCard +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * The [Store] for holding the [CreditCardsListState] and applying [CreditCardsAction]s. + */ +class CreditCardsFragmentStore(initialState: CreditCardsListState) : + Store( + initialState, ::creditCardsFragmentStateReducer + ) + +/** + * The state for [CreditCardsManagementFragment]. + * + * @property creditCards The list of [CreditCard]s to display in the credit card list. + * @property isLoading True if the credit cards are still being loaded from storage, + * otherwise false. + */ +data class CreditCardsListState( + val creditCards: List, + val isLoading: Boolean = true +) : State + +/** + * Actions to dispatch through the [CreditCardsFragmentStore] to modify the [CreditCardsListState] + * through the [creditCardsFragmentStateReducer]. + */ +sealed class CreditCardsAction : Action { + /** + * Updates the list of credit cards with the provided [creditCards]. + * + * @param creditCards The list of [CreditCard]s to display in the credit card list. + */ + data class UpdateCreditCards(val creditCards: List) : CreditCardsAction() +} + +/** + * Reduces the credit cards state from the current state with the provided [action] to be performed. + * + * @param state The current credit cards state. + * @param action The action to be performed on the state. + * @return the new [CreditCardsListState] with the [action] executed. + */ +private fun creditCardsFragmentStateReducer( + state: CreditCardsListState, + action: CreditCardsAction +): CreditCardsListState { + return when (action) { + is CreditCardsAction.UpdateCreditCards -> { + state.copy( + creditCards = action.creditCards, + isLoading = false + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsManagementFragment.kt new file mode 100644 index 000000000..be0ceaffa --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsManagementFragment.kt @@ -0,0 +1,86 @@ +/* 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.settings.creditcards + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.fragment_saved_cards.view.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import mozilla.components.lib.state.ext.consumeFrom +import org.mozilla.fenix.R +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.creditcards.controller.DefaultCreditCardsManagementController +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardsManagementInteractor +import org.mozilla.fenix.settings.creditcards.interactor.DefaultCreditCardsManagementInteractor +import org.mozilla.fenix.settings.creditcards.view.CreditCardsManagementView + +/** + * Displays a list of saved credit cards. + */ +class CreditCardsManagementFragment : Fragment() { + + private lateinit var creditCardsStore: CreditCardsFragmentStore + private lateinit var interactor: CreditCardsManagementInteractor + private lateinit var creditCardsView: CreditCardsManagementView + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val view = inflater.inflate(R.layout.fragment_saved_cards, container, false) + + creditCardsStore = StoreProvider.get(this) { + CreditCardsFragmentStore(CreditCardsListState(creditCards = emptyList())) + } + + interactor = DefaultCreditCardsManagementInteractor( + controller = DefaultCreditCardsManagementController( + navController = findNavController() + ) + ) + + creditCardsView = CreditCardsManagementView(view.saved_cards_layout, interactor) + + loadCreditCards() + + return view + } + + @ExperimentalCoroutinesApi + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + consumeFrom(creditCardsStore) { state -> + creditCardsView.update(state) + } + } + + override fun onResume() { + super.onResume() + showToolbar(getString(R.string.credit_cards_saved_cards)) + } + + /** + * Fetches all the credit cards from the autofill storage and updates the + * [CreditCardsFragmentStore] with the list of credit cards. + */ + private fun loadCreditCards() { + lifecycleScope.launch(Dispatchers.IO) { + val creditCards = requireContext().components.core.autofillStorage.getAllCreditCards() + + lifecycleScope.launch(Dispatchers.Main) { + creditCardsStore.dispatch(CreditCardsAction.UpdateCreditCards(creditCards)) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsSettingFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsSettingFragment.kt new file mode 100644 index 000000000..bcb197137 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/CreditCardsSettingFragment.kt @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards + +import android.os.Bundle +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import mozilla.components.service.fxa.SyncEngine +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.SyncPreferenceView +import org.mozilla.fenix.settings.requirePreference + +/** + * "Credit cards" settings fragment displays a list of settings related to autofilling, adding and + * syncing credit cards. + */ +class CreditCardsSettingFragment : PreferenceFragmentCompat() { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.credit_cards_preferences, rootKey) + } + + override fun onResume() { + super.onResume() + + showToolbar(getString(R.string.preferences_credit_cards)) + + SyncPreferenceView( + syncPreference = requirePreference(R.string.pref_key_credit_cards_sync_cards_across_devices), + lifecycleOwner = viewLifecycleOwner, + accountManager = requireComponents.backgroundServices.accountManager, + syncEngine = SyncEngine.Passwords, + onSignInToSyncClicked = { + val directions = + CreditCardsSettingFragmentDirections.actionCreditCardsSettingFragmentToTurnOnSyncFragment() + findNavController().navigateBlockingForAsyncNavGraph(directions) + }, + onSyncStatusClicked = { + val directions = + CreditCardsSettingFragmentDirections.actionGlobalAccountSettingsFragment() + findNavController().navigateBlockingForAsyncNavGraph(directions) + }, + onReconnectClicked = { + val directions = + CreditCardsSettingFragmentDirections.actionGlobalAccountProblemFragment() + findNavController().navigateBlockingForAsyncNavGraph(directions) + } + ) + } + + @Suppress("MaxLineLength") + override fun onPreferenceTreeClick(preference: Preference): Boolean { + when (preference.key) { + getPreferenceKey(R.string.pref_key_credit_cards_add_credit_card) -> { + val directions = + CreditCardsSettingFragmentDirections + .actionCreditCardsSettingFragmentToCreditCardEditorFragment() + findNavController().navigateBlockingForAsyncNavGraph(directions) + } + getPreferenceKey(R.string.pref_key_credit_cards_manage_saved_cards) -> { + val directions = + CreditCardsSettingFragmentDirections + .actionCreditCardsSettingFragmentToCreditCardsManagementFragment() + findNavController().navigateBlockingForAsyncNavGraph(directions) + } + } + + return super.onPreferenceTreeClick(preference) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt new file mode 100644 index 000000000..bfa6ca66b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardEditorController.kt @@ -0,0 +1,93 @@ +/* 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.settings.creditcards.controller + +import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.service.sync.autofill.AutofillCreditCardsAddressesStorage +import org.mozilla.fenix.settings.creditcards.CreditCardEditorFragment +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardEditorInteractor + +/** + * [CreditCardEditorFragment] controller. An interface that handles the view manipulation of the + * credit card editor. + */ +interface CreditCardEditorController { + + /** + * @see [CreditCardEditorInteractor.onCancelButtonClicked] + */ + fun handleCancelButtonClicked() + + /** + * @see [CreditCardEditorInteractor.onDeleteCardButtonClicked] + */ + fun handleDeleteCreditCard(guid: String) + + /** + * @see [CreditCardEditorInteractor.onSaveCreditCard] + */ + fun handleSaveCreditCard(creditCardFields: UpdatableCreditCardFields) + + /** + * @see [CreditCardEditorInteractor.onUpdateCreditCard] + */ + fun handleUpdateCreditCard(guid: String, creditCardFields: UpdatableCreditCardFields) +} + +/** + * The default implementation of [CreditCardEditorController]. + * + * @param storage An instance of the [AutofillCreditCardsAddressesStorage] for adding and retrieving + * credit cards. + * @param lifecycleScope [CoroutineScope] scope to launch coroutines. + * @param navController [NavController] used for navigation. + * @param ioDispatcher [CoroutineDispatcher] used for executing async tasks. Defaults to [Dispatchers.IO]. + */ +class DefaultCreditCardEditorController( + private val storage: AutofillCreditCardsAddressesStorage, + private val lifecycleScope: CoroutineScope, + private val navController: NavController, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : CreditCardEditorController { + + override fun handleCancelButtonClicked() { + navController.popBackStack() + } + + override fun handleDeleteCreditCard(guid: String) { + lifecycleScope.launch(ioDispatcher) { + storage.deleteCreditCard(guid) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } + + override fun handleSaveCreditCard(creditCardFields: UpdatableCreditCardFields) { + lifecycleScope.launch(ioDispatcher) { + storage.addCreditCard(creditCardFields) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } + + override fun handleUpdateCreditCard(guid: String, creditCardFields: UpdatableCreditCardFields) { + lifecycleScope.launch(ioDispatcher) { + storage.updateCreditCard(guid, creditCardFields) + + lifecycleScope.launch(Dispatchers.Main) { + navController.popBackStack() + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardsManagementController.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardsManagementController.kt new file mode 100644 index 000000000..bcc0906a0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/controller/CreditCardsManagementController.kt @@ -0,0 +1,41 @@ +/* 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.settings.creditcards.controller + +import androidx.navigation.NavController +import mozilla.components.concept.storage.CreditCard +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import org.mozilla.fenix.settings.creditcards.CreditCardsManagementFragment +import org.mozilla.fenix.settings.creditcards.CreditCardsManagementFragmentDirections +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardsManagementInteractor + +/** + * [CreditCardsManagementFragment] controller. An interface that handles the view manipulation of + * the credit cards manager triggered by the Interactor. + */ +interface CreditCardsManagementController { + + /** + * @see [CreditCardsManagementInteractor.onSelectCreditCard] + */ + fun handleCreditCardClicked(creditCard: CreditCard) +} + +/** + * The default implementation of [CreditCardsManagementController]. + */ +class DefaultCreditCardsManagementController( + private val navController: NavController +) : CreditCardsManagementController { + + override fun handleCreditCardClicked(creditCard: CreditCard) { + navController.navigateBlockingForAsyncNavGraph( + CreditCardsManagementFragmentDirections + .actionCreditCardsManagementFragmentToCreditCardEditorFragment( + creditCard = creditCard + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt new file mode 100644 index 000000000..2633c57fe --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardEditorInteractor.kt @@ -0,0 +1,72 @@ +/* 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.settings.creditcards.interactor + +import mozilla.components.concept.storage.UpdatableCreditCardFields +import org.mozilla.fenix.settings.creditcards.controller.CreditCardEditorController + +/** + * Interface for the credit card editor Interactor. + */ +interface CreditCardEditorInteractor { + + /** + * Navigates back to the credit card preference settings. Called when a user taps on the + * "Cancel" button. + */ + fun onCancelButtonClicked() + + /** + * Deletes the provided credit card in the credit card storage. Called when a user + * taps on the delete menu item or "Delete card" button. + * + * @param guid Unique identifier for the credit card to be deleted. + */ + fun onDeleteCardButtonClicked(guid: String) + + /** + * Saves the provided credit card field into the credit card storage. Called when a user + * taps on the save menu item or "Save" button. + * + * @param creditCardFields A [UpdatableCreditCardFields] record to add. + */ + fun onSaveCreditCard(creditCardFields: UpdatableCreditCardFields) + + /** + * Updates the provided credit card with the new credit card fields. Called when a user + * taps on the save menu item or "Save" button when editing an existing credit card. + * + * @param guid Unique identifier for the desired credit card. + * @param creditCardFields The credit card fields to update. + */ + fun onUpdateCreditCard(guid: String, creditCardFields: UpdatableCreditCardFields) +} + +/** + * The default implementation of [CreditCardEditorInteractor]. + * + * @param controller An instance of [CreditCardEditorController] which will be delegated for all + * user interactions. + */ +class DefaultCreditCardEditorInteractor( + private val controller: CreditCardEditorController +) : CreditCardEditorInteractor { + + override fun onCancelButtonClicked() { + controller.handleCancelButtonClicked() + } + + override fun onDeleteCardButtonClicked(guid: String) { + controller.handleDeleteCreditCard(guid) + } + + override fun onSaveCreditCard(creditCardFields: UpdatableCreditCardFields) { + controller.handleSaveCreditCard(creditCardFields) + } + + override fun onUpdateCreditCard(guid: String, creditCardFields: UpdatableCreditCardFields) { + controller.handleUpdateCreditCard(guid, creditCardFields) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt new file mode 100644 index 000000000..251d9de8c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/interactor/CreditCardsManagementInteractor.kt @@ -0,0 +1,37 @@ +/* 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.settings.creditcards.interactor + +import mozilla.components.concept.storage.CreditCard +import org.mozilla.fenix.settings.creditcards.controller.CreditCardsManagementController + +/** + * Interface for the credit cards management Interactor. + */ +interface CreditCardsManagementInteractor { + + /** + * Navigates to the credit card editor to edit the selected credit card. Called when a user + * taps on a credit card item. + * + * @param creditCard The selected [CreditCard] to edit. + */ + fun onSelectCreditCard(creditCard: CreditCard) +} + +/** + * The default implementation of [CreditCardsManagementInteractor]. + * + * @param controller An instance of [CreditCardsManagementController] which will be delegated for + * all user interactions. + */ +class DefaultCreditCardsManagementInteractor( + private val controller: CreditCardsManagementController +) : CreditCardsManagementInteractor { + + override fun onSelectCreditCard(creditCard: CreditCard) { + controller.handleCreditCardClicked(creditCard) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt new file mode 100644 index 000000000..adf19410c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardEditorView.kt @@ -0,0 +1,118 @@ +/* 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.settings.creditcards.view + +import android.R +import android.view.View +import android.widget.ArrayAdapter +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.fragment_credit_card_editor.* +import mozilla.components.concept.storage.UpdatableCreditCardFields +import mozilla.components.support.ktx.android.view.hideKeyboard +import org.mozilla.fenix.ext.toEditable +import org.mozilla.fenix.settings.creditcards.CreditCardEditorFragment.Companion.CARD_TYPE_PLACEHOLDER +import org.mozilla.fenix.settings.creditcards.CreditCardEditorState +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardEditorInteractor +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +/** + * Shows a credit card editor for adding or updating a credit card. + */ +class CreditCardEditorView( + override val containerView: View, + private val interactor: CreditCardEditorInteractor +) : LayoutContainer { + + /** + * Binds the given [CreditCardEditorState] in the [CreditCardEditorFragment]. + */ + fun bind(state: CreditCardEditorState) { + if (state.isEditing) { + delete_button.apply { + visibility = View.VISIBLE + + setOnClickListener { + interactor.onDeleteCardButtonClicked(state.guid) + } + } + } + + cancel_button.setOnClickListener { + interactor.onCancelButtonClicked() + } + + save_button.setOnClickListener { + containerView.hideKeyboard() + + val creditCardFields = UpdatableCreditCardFields( + billingName = name_on_card_input.text.toString(), + cardNumber = card_number_input.text.toString(), + expiryMonth = (expiry_month_drop_down.selectedItemPosition + 1).toLong(), + expiryYear = expiry_year_drop_down.selectedItem.toString().toLong(), + cardType = CARD_TYPE_PLACEHOLDER + ) + + if (state.isEditing) { + interactor.onUpdateCreditCard(state.guid, creditCardFields) + } else { + interactor.onSaveCreditCard(creditCardFields) + } + } + + card_number_input.text = state.cardNumber.toEditable() + name_on_card_input.text = state.billingName.toEditable() + + bindExpiryMonthDropDown(state.expiryMonth) + bindExpiryYearDropDown(state.expiryYears) + } + + /** + * Setup the expiry month dropdown by formatting and populating it with the months in a calendar + * year, and set the selection to the provided expiry month. + * + * @param expiryMonth The selected credit card expiry month to display. + */ + private fun bindExpiryMonthDropDown(expiryMonth: Int) { + val adapter = + ArrayAdapter(containerView.context, R.layout.simple_spinner_dropdown_item) + val dateFormat = SimpleDateFormat("MMMM (MM)", Locale.getDefault()) + + val calendar = Calendar.getInstance() + calendar.set(Calendar.DAY_OF_MONTH, 1) + + for (month in 0..NUMBER_OF_MONTHS) { + calendar.set(Calendar.MONTH, month) + adapter.add(dateFormat.format(calendar.time)) + } + + expiry_month_drop_down.adapter = adapter + expiry_month_drop_down.setSelection(expiryMonth - 1) + } + + /** + * Setup the expiry year dropdown with the range specified by the provided expiryYears + * + * @param expiryYears A range specifying the start and end year to display in the expiry year + * dropdown. + */ + private fun bindExpiryYearDropDown(expiryYears: Pair) { + val adapter = + ArrayAdapter(containerView.context, R.layout.simple_spinner_dropdown_item) + val (startYear, endYear) = expiryYears + + for (year in startYear until endYear) { + adapter.add(year.toString()) + } + + expiry_year_drop_down.adapter = adapter + } + + companion object { + // Number of months in a year (0-indexed). + const val NUMBER_OF_MONTHS = 11 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardItemViewHolder.kt new file mode 100644 index 000000000..b5d18ebe0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardItemViewHolder.kt @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards.view + +import android.view.View +import kotlinx.android.synthetic.main.credit_card_list_item.* +import mozilla.components.concept.storage.CreditCard +import org.mozilla.fenix.R +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardsManagementInteractor +import org.mozilla.fenix.utils.view.ViewHolder +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +/** + * View holder for a credit card list item. + */ +class CreditCardItemViewHolder( + view: View, + private val interactor: CreditCardsManagementInteractor +) : ViewHolder(view) { + + fun bind(creditCard: CreditCard) { + credit_card_number.text = creditCard.cardNumber + + bindCreditCardExpiryDate(creditCard) + + itemView.setOnClickListener { + interactor.onSelectCreditCard(creditCard) + } + } + + /** + * Set the credit card expiry date formatted according to the locale. + */ + private fun bindCreditCardExpiryDate(creditCard: CreditCard) { + val dateFormat = SimpleDateFormat(DATE_PATTERN, Locale.getDefault()) + + val calendar = Calendar.getInstance() + calendar.set(Calendar.DAY_OF_MONTH, 1) + // Subtract 1 from the expiry month since Calendar.Month is based on a 0-indexed. + calendar.set(Calendar.MONTH, creditCard.expiryMonth.toInt() - 1) + calendar.set(Calendar.YEAR, creditCard.expiryYear.toInt()) + + expiry_date.text = dateFormat.format(calendar.time) + } + + companion object { + const val LAYOUT_ID = R.layout.credit_card_list_item + + // Date format pattern for the credit card expiry date. + private const val DATE_PATTERN = "MM/yyyy" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsAdapter.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsAdapter.kt new file mode 100644 index 000000000..55d5c0328 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsAdapter.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.settings.creditcards.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import mozilla.components.concept.storage.CreditCard +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardsManagementInteractor + +/** + * Adapter for a list of credit cards to be displayed. + */ +class CreditCardsAdapter( + private val interactor: CreditCardsManagementInteractor +) : ListAdapter(DiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CreditCardItemViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(CreditCardItemViewHolder.LAYOUT_ID, parent, false) + return CreditCardItemViewHolder(view, interactor) + } + + override fun onBindViewHolder(holder: CreditCardItemViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + internal object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: CreditCard, newItem: CreditCard) = + oldItem.guid == newItem.guid + + override fun areContentsTheSame(oldItem: CreditCard, newItem: CreditCard) = + oldItem == newItem + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsManagementView.kt b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsManagementView.kt new file mode 100644 index 000000000..8bc175076 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/settings/creditcards/view/CreditCardsManagementView.kt @@ -0,0 +1,49 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.settings.creditcards.view + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.android.extensions.LayoutContainer +import kotlinx.android.synthetic.main.component_credit_cards.* +import org.mozilla.fenix.R +import org.mozilla.fenix.settings.creditcards.CreditCardsListState +import org.mozilla.fenix.settings.creditcards.interactor.CreditCardsManagementInteractor + +/** + * Shows a list of credit cards. + */ +class CreditCardsManagementView( + override val containerView: ViewGroup, + val interactor: CreditCardsManagementInteractor +) : LayoutContainer { + + private val creditCardsAdapter = CreditCardsAdapter(interactor) + + init { + LayoutInflater.from(containerView.context).inflate(LAYOUT_ID, containerView, true) + + credit_cards_list.apply { + adapter = creditCardsAdapter + layoutManager = LinearLayoutManager(containerView.context) + } + } + + /** + * Updates the display of the credit cards based on the given [CreditCardsListState]. + */ + fun update(state: CreditCardsListState) { + progress_bar.isVisible = state.isLoading + credit_cards_list.isVisible = state.creditCards.isNotEmpty() + + creditCardsAdapter.submitList(state.creditCards) + } + + companion object { + const val LAYOUT_ID = R.layout.component_credit_cards + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt index e5fe0ac42..d7f8a6d6e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt @@ -49,7 +49,7 @@ fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar: snackbar?.dismiss() - activity.finish() + activity.finishAndRemoveTask() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index 4bb7dcd1c..a1d0fd7c0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -28,6 +28,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -199,7 +200,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da // If the user deletes all open tabs we need to make sure we remove // the BrowserFragment from the backstack. popBackStack(R.id.homeFragment, false) - navigate(DeleteBrowsingDataFragmentDirections.actionGlobalSettingsFragment()) + navigateBlockingForAsyncNavGraph(DeleteBrowsingDataFragmentDirections.actionGlobalSettingsFragment()) } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt index 8d4c5629f..2f662ecca 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/LoginsListController.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavController import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsFragmentStore @@ -40,7 +41,7 @@ class LoginsListController( fun handleItemClicked(item: SavedLogin) { loginsFragmentStore.dispatch(LoginsAction.LoginSelected(item)) metrics.track(Event.OpenOneLogin) - navController.navigate( + navController.navigateBlockingForAsyncNavGraph( SavedLoginsFragmentDirections.actionSavedLoginsFragmentToLoginDetailFragment(item.guid) ) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt index 5257c73f8..885a2ce90 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/controller/SavedLoginsStorageController.kt @@ -20,6 +20,7 @@ import mozilla.components.service.sync.logins.LoginsStorageException import mozilla.components.service.sync.logins.NoSuchRecordException import mozilla.components.service.sync.logins.SyncableLoginsStorage import org.mozilla.fenix.R +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsFragmentStore import org.mozilla.fenix.settings.logins.fragment.EditLoginFragmentDirections @@ -83,7 +84,7 @@ open class SavedLoginsStorageController( EditLoginFragmentDirections.actionEditLoginFragmentToLoginDetailFragment( loginId ) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } } saveLoginJob?.invokeOnCompletion { diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt index 9adcee40f..36b5844db 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/EditLoginFragment.kt @@ -30,6 +30,7 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.ext.toEditable import org.mozilla.fenix.settings.logins.LoginsAction import org.mozilla.fenix.settings.logins.LoginsFragmentStore import org.mozilla.fenix.settings.logins.SavedLogin @@ -45,8 +46,6 @@ import org.mozilla.fenix.settings.logins.togglePasswordReveal @Suppress("TooManyFunctions", "NestedBlockDepth", "ForbiddenComment") class EditLoginFragment : Fragment(R.layout.fragment_edit_login) { - private fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this) - private val args by navArgs() private lateinit var loginsFragmentStore: LoginsFragmentStore private lateinit var interactor: EditLoginInteractor 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 10b031877..3826179c0 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 @@ -33,6 +33,7 @@ import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.increaseTapArea +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.redirectToReAuth import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -199,7 +200,7 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) { LoginDetailFragmentDirections.actionLoginDetailFragmentToEditLoginFragment( login!! ) - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } private fun displayDeleteLoginDialog() { 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 021ef626d..f92d070d4 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 @@ -27,6 +27,7 @@ import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.secure @@ -130,17 +131,17 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { onSignInToSyncClicked = { val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToTurnOnSyncFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) }, onSyncStatusClicked = { val directions = SavedLoginsAuthFragmentDirections.actionGlobalAccountSettingsFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) }, onReconnectClicked = { val directions = SavedLoginsAuthFragmentDirections.actionGlobalAccountProblemFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } ) @@ -213,19 +214,19 @@ class SavedLoginsAuthFragment : PreferenceFragmentCompat() { context?.components?.analytics?.metrics?.track(Event.OpenLogins) val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginsListFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } private fun navigateToSaveLoginSettingFragment() { val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToSavedLoginsSettingFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } private fun navigateToLoginExceptionFragment() { val directions = SavedLoginsAuthFragmentDirections.actionSavedLoginsAuthFragmentToLoginExceptionsFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } companion object { diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index f93b9c69f..c8dbe8ac4 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -18,6 +18,7 @@ import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase import mozilla.components.support.base.feature.OnNeedToRequestPermissions import org.mozilla.fenix.components.PermissionStorage +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.quicksettings.ext.shouldBeEnabled import org.mozilla.fenix.settings.toggle @@ -199,6 +200,6 @@ class DefaultQuickSettingsController( private fun navigateToManagePhoneFeature(phoneFeature: PhoneFeature) { val directions = QuickSettingsSheetDialogFragmentDirections .actionGlobalSitePermissionsManagePhoneFeature(phoneFeature) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt b/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt index caea80564..fb79c79cf 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/RadioSearchEngineListPreference.kt @@ -38,6 +38,7 @@ import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getRootView +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.utils.allowUndo class RadioSearchEngineListPreference @JvmOverloads constructor( @@ -146,7 +147,7 @@ class RadioSearchEngineListPreference @JvmOverloads constructor( val directions = SearchEngineFragmentDirections .actionSearchEngineFragmentToEditCustomSearchEngineFragment(engine.id) - Navigation.findNavController(view).navigate(directions) + Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions) } private fun deleteSearchEngine( diff --git a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt index c5971e7f9..41d648b57 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/search/SearchEngineFragment.kt @@ -13,6 +13,7 @@ import androidx.preference.SwitchPreference import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.SharedPreferenceUpdater @@ -99,7 +100,7 @@ class SearchEngineFragment : PreferenceFragmentCompat() { getPreferenceKey(R.string.pref_key_add_search_engine) -> { val directions = SearchEngineFragmentDirections .actionSearchEngineFragmentToAddSearchEngineFragment() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt index 399d46ca8..49b7cdb43 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.withContext import mozilla.components.feature.sitepermissions.SitePermissions import org.mozilla.fenix.R import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -161,6 +162,6 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { phoneFeature = phoneFeature, sitePermissions = sitePermissions ) - requireView().findNavController().navigate(directions) + requireView().findNavController().navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt index a87f1041e..688766782 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsFragment.kt @@ -12,6 +12,7 @@ import androidx.preference.PreferenceFragmentCompat import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar @@ -42,7 +43,7 @@ class SitePermissionsFragment : PreferenceFragmentCompat() { exceptionsCategory.onPreferenceClickListener = OnPreferenceClickListener { val directions = SitePermissionsFragmentDirections.actionSitePermissionsToExceptions() - Navigation.findNavController(requireView()).navigate(directions) + Navigation.findNavController(requireView()).navigateBlockingForAsyncNavGraph(directions) true } } @@ -59,17 +60,8 @@ class SitePermissionsFragment : PreferenceFragmentCompat() { val context = requireContext() val settings = context.settings() - val summary = phoneFeature.getActionLabel(context, settings = settings) - // Remove autoplaySummary after https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed - val autoplaySummary = - if (summary == context.getString(R.string.preference_option_autoplay_allowed2)) { - context.getString(R.string.preference_option_autoplay_allowed_wifi_only2) - } else { - null - } - val cameraPhoneFeatures = requirePreference(phoneFeature.getPreferenceId()) - cameraPhoneFeatures.summary = autoplaySummary ?: summary + cameraPhoneFeatures.summary = phoneFeature.getActionLabel(context, settings = settings) cameraPhoneFeatures.onPreferenceClickListener = OnPreferenceClickListener { navigateToPhoneFeature(phoneFeature) @@ -85,6 +77,6 @@ class SitePermissionsFragment : PreferenceFragmentCompat() { requireComponents.analytics.metrics.track(Event.AutoPlaySettingVisited) } - Navigation.findNavController(requireView()).navigate(directions) + Navigation.findNavController(requireView()).navigateBlockingForAsyncNavGraph(directions) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt index 2644e8ac9..a5f1bd0a6 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManagePhoneFeatureFragment.kt @@ -79,14 +79,12 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { private fun initFirstRadio(rootView: View) { with(rootView.ask_to_allow_radio) { if (args.phoneFeature == AUTOPLAY_AUDIBLE) { - // Disabled because GV does not allow this setting. TODO Reenable after - // https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed -// text = getString(R.string.preference_option_autoplay_allowed2) -// setOnClickListener { -// saveActionInSettings(it.context, AUTOPLAY_ALLOW_ALL) -// } -// restoreState(AUTOPLAY_ALLOW_ALL) - visibility = View.GONE + text = getString(R.string.preference_option_autoplay_allowed2) + setOnClickListener { + saveActionInSettings(AUTOPLAY_ALLOW_ALL) + } + restoreState(AUTOPLAY_ALLOW_ALL) + visibility = View.VISIBLE } else { text = getCombinedLabel( getString(R.string.preference_option_phone_feature_ask_to_allow), @@ -109,10 +107,7 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { getString(R.string.preference_option_autoplay_allowed_wifi_subtext) ) setOnClickListener { - // TODO replace with AUTOPLAY_ALLOW_ON_WIFI when - // https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed. This GV bug - // makes ALLOW_ALL behave as ALLOW_ON_WIFI - saveActionInSettings(AUTOPLAY_ALLOW_ALL) + saveActionInSettings(AUTOPLAY_ALLOW_ON_WIFI) } restoreState(AUTOPLAY_ALLOW_ON_WIFI) } else { @@ -129,7 +124,10 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { with(rootView.third_radio) { if (args.phoneFeature == AUTOPLAY_AUDIBLE) { visibility = View.VISIBLE - text = getString(R.string.preference_option_autoplay_block_audio2) + text = getCombinedLabel( + getString(R.string.preference_option_autoplay_block_audio2), + getString(R.string.phone_feature_recommended) + ) setOnClickListener { saveActionInSettings(AUTOPLAY_BLOCK_AUDIBLE) } @@ -151,10 +149,8 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { with(rootView.fourth_radio) { if (args.phoneFeature == AUTOPLAY_AUDIBLE) { visibility = View.VISIBLE - text = getCombinedLabel( - getString(R.string.preference_option_autoplay_blocked3), - getString(R.string.phone_feature_recommended) - ) + text = getString(R.string.preference_option_autoplay_blocked3) + setOnClickListener { saveActionInSettings(AUTOPLAY_BLOCK_ALL) } @@ -173,7 +169,7 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { } private fun RadioButton.restoreState(buttonAutoplaySetting: Int) { - if (settings.getAutoplayUserSetting(AUTOPLAY_BLOCK_ALL) == buttonAutoplaySetting) { + if (settings.getAutoplayUserSetting() == buttonAutoplaySetting) { this.isChecked = true this.setStartCheckedIndicator() } @@ -194,9 +190,11 @@ class SitePermissionsManagePhoneFeatureFragment : Fragment() { val setting: Event.AutoPlaySettingChanged.AutoplaySetting val (audible, inaudible) = when (autoplaySetting) { - AUTOPLAY_ALLOW_ALL, + AUTOPLAY_ALLOW_ALL -> { + setting = Event.AutoPlaySettingChanged.AutoplaySetting.ALLOW_ALL + ALLOWED to ALLOWED + } AUTOPLAY_ALLOW_ON_WIFI -> { - settings.setAutoplayUserSetting(AUTOPLAY_ALLOW_ON_WIFI) setting = Event.AutoPlaySettingChanged.AutoplaySetting.BLOCK_CELLULAR BLOCKED to BLOCKED } diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt index bb7c72643..5607edf4d 100644 --- a/app/src/main/java/org/mozilla/fenix/share/ShareController.kt +++ b/app/src/main/java/org/mozilla/fenix/share/ShareController.kt @@ -31,6 +31,7 @@ import mozilla.components.support.ktx.kotlin.isExtensionUrl import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.share.listadapters.AppShareOption @@ -121,7 +122,7 @@ class DefaultShareController( override fun handleAddNewDevice() { val directions = ShareFragmentDirections.actionShareFragmentToAddNewDeviceFragment() - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } override fun handleShareToDevice(device: Device) { 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 a48d6d264..375cec1e6 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsViewHolder.kt @@ -19,6 +19,7 @@ import mozilla.components.concept.sync.DeviceType import mozilla.components.feature.syncedtabs.view.SyncedTabsView import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.sync.SyncedTabsAdapter.AdapterItem /** @@ -63,7 +64,7 @@ sealed class SyncedTabsViewHolder(itemView: View) : RecyclerView.ViewHolder(item errorItem.navController?.let { navController -> itemView.sync_tabs_error_cta_button.visibility = VISIBLE itemView.sync_tabs_error_cta_button.setOnClickListener { - navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) + navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalTurnOnSync()) } } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt new file mode 100644 index 000000000..d19958375 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/CloseOnLastTabBinding.kt @@ -0,0 +1,50 @@ +/* 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.tabstray + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.components.AbstractBinding + +/** + * A binding that closes the tabs tray when the last tab is closed. + */ +class CloseOnLastTabBinding( + browserStore: BrowserStore, + private val tabsTrayStore: TabsTrayStore, + private val navigationInteractor: NavigationInteractor +) : AbstractBinding(browserStore) { + override suspend fun onState(flow: Flow) { + flow.map { it } + // Ignore the initial state; we don't want to close immediately. + .drop(1) + .ifChanged { it.tabs } + .collect { state -> + val selectedPage = tabsTrayStore.state.selectedPage + val tabs = when (selectedPage) { + Page.NormalTabs -> { + state.normalTabs + } + Page.PrivateTabs -> { + state.privateTabs + } + else -> { + // Do nothing if we're on any other non-browser page. + null + } + } + if (tabs?.isEmpty() == true) { + navigationInteractor.onCloseAllTabsClicked(selectedPage == Page.PrivateTabs) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/FloatingActionButtonBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/FloatingActionButtonBinding.kt new file mode 100644 index 000000000..42d925368 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/FloatingActionButtonBinding.kt @@ -0,0 +1,90 @@ +/* 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.tabstray + +import androidx.appcompat.content.res.AppCompatResources +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.fenix.R +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor + +class FloatingActionButtonBinding( + private val store: TabsTrayStore, + private val actionButton: ExtendedFloatingActionButton, + private val browserTrayInteractor: BrowserTrayInteractor +) : LifecycleAwareFeature { + + private var scope: CoroutineScope? = null + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + setFab(store.state.selectedPage, store.state.syncing) + scope = store.flowScoped { flow -> + flow.map { it } + .ifAnyChanged { state -> + arrayOf( + state.selectedPage, + state.syncing + ) + } + .collect { state -> + setFab(state.selectedPage, state.syncing) + } + } + } + + override fun stop() { + scope?.cancel() + } + + private fun setFab(selectedPage: Page, syncing: Boolean) { + when (selectedPage) { + Page.NormalTabs -> { + actionButton.apply { + shrink() + show() + icon = AppCompatResources.getDrawable(context, R.drawable.ic_new) + setOnClickListener { + browserTrayInteractor.onFabClicked(false) + } + } + } + Page.PrivateTabs -> { + actionButton.apply { + text = context.getText(R.string.tab_drawer_fab_content) + extend() + show() + icon = AppCompatResources.getDrawable(context, R.drawable.ic_new) + setOnClickListener { + browserTrayInteractor.onFabClicked(true) + } + } + } + Page.SyncedTabs -> { + actionButton.apply { + text = if (syncing) context.getText(R.string.sync_syncing_in_progress) + else context.getText(R.string.tab_drawer_fab_sync) + extend() + show() + icon = AppCompatResources.getDrawable(context, R.drawable.ic_fab_sync) + setOnClickListener { + // Notify the store observers (one of which is the SyncedTabsFeature), that + // a sync was requested. + if (!syncing) { + store.dispatch(TabsTrayAction.SyncNow) + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt new file mode 100644 index 000000000..2a1630204 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/MenuIntegration.kt @@ -0,0 +1,59 @@ +/* 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.tabstray + +import android.content.Context +import androidx.annotation.VisibleForTesting +import com.google.android.material.tabs.TabLayout +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.utils.Do + +/** + * A wrapper class that building the tabs tray menu that handles item clicks. + */ +class MenuIntegration( + private val context: Context, + private val browserStore: BrowserStore, + private val tabsTrayStore: TabsTrayStore, + private val tabLayout: TabLayout, + private val navigationInteractor: NavigationInteractor +) { + private val tabsTrayItemMenu by lazy { + TabsTrayMenu( + context = context, + browserStore = browserStore, + tabLayout = tabLayout, + onItemTapped = ::handleMenuClicked + ) + } + + private val isPrivateMode: Boolean + get() = tabsTrayStore.state.selectedPage == Page.PrivateTabs + + /** + * Builds the internal menu items list. See [BrowserMenuBuilder.build]. + */ + fun build() = tabsTrayItemMenu.menuBuilder.build(context) + + @VisibleForTesting + internal fun handleMenuClicked(item: TabsTrayMenu.Item) { + Do exhaustive when (item) { + is TabsTrayMenu.Item.ShareAllTabs -> + navigationInteractor.onShareTabsOfTypeClicked(isPrivateMode) + is TabsTrayMenu.Item.OpenAccountSettings -> + navigationInteractor.onAccountSettingsClicked() + is TabsTrayMenu.Item.OpenTabSettings -> + navigationInteractor.onTabSettingsClicked() + is TabsTrayMenu.Item.CloseAllTabs -> + navigationInteractor.onCloseAllTabsClicked(isPrivateMode) + is TabsTrayMenu.Item.OpenRecentlyClosed -> + navigationInteractor.onOpenRecentlyClosedClicked() + is TabsTrayMenu.Item.SelectTabs -> { + tabsTrayStore.dispatch(TabsTrayAction.EnterSelectMode) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/NavigationInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/NavigationInteractor.kt new file mode 100644 index 000000000..c88c42d94 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/NavigationInteractor.kt @@ -0,0 +1,214 @@ +/* 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.tabstray + +import android.content.Context +import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.storage.sync.Tab as SyncTab +import mozilla.components.concept.engine.prompt.ShareData +import mozilla.components.concept.tabstray.Tab +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.collections.CollectionsDialog +import org.mozilla.fenix.collections.show +import org.mozilla.fenix.components.TabCollectionStorage +import org.mozilla.fenix.components.bookmarks.BookmarksUseCase +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import org.mozilla.fenix.home.HomeFragment +import org.mozilla.fenix.tabstray.ext.getTabSessionState + +/** + * An interactor that helps with navigating to different parts of the app from the tabs tray. + */ +interface NavigationInteractor { + + /** + * Called when tab tray should be dismissed. + */ + fun onTabTrayDismissed() + + /** + * Called when clicking the account settings button. + */ + fun onAccountSettingsClicked() + + /** + * Called when sharing a list of [Tab]s. + */ + fun onShareTabs(tabs: Collection) + + /** + * Called when clicking the share tabs button. + */ + fun onShareTabsOfTypeClicked(private: Boolean) + + /** + * Called when clicking the tab settings button. + */ + fun onTabSettingsClicked() + + /** + * Called when clicking the close all tabs button. + */ + fun onCloseAllTabsClicked(private: Boolean) + + /** + * Called when opening the recently closed tabs menu button. + */ + fun onOpenRecentlyClosedClicked() + + /** + * Used when opening the add-to-collections user flow. + */ + fun onSaveToCollections(tabs: Collection) + + /** + * Used when adding [Tab]s as bookmarks. + */ + fun onSaveToBookmarks(tabs: Collection) + + /** + * Called when clicking on a SyncedTab item. + */ + fun onSyncedTabClicked(tab: SyncTab) +} + +/** + * A default implementation of [NavigationInteractor]. + */ +@Suppress("LongParameterList") +class DefaultNavigationInteractor( + private val context: Context, + private val activity: HomeActivity, + private val browserStore: BrowserStore, + private val navController: NavController, + private val metrics: MetricController, + private val dismissTabTray: () -> Unit, + private val dismissTabTrayAndNavigateHome: (String) -> Unit, + private val bookmarksUseCase: BookmarksUseCase, + private val tabsTrayStore: TabsTrayStore, + private val collectionStorage: TabCollectionStorage, + private val accountManager: FxaAccountManager +) : NavigationInteractor { + + override fun onTabTrayDismissed() { + dismissTabTray() + } + + override fun onAccountSettingsClicked() { + val isSignedIn = accountManager.authenticatedAccount() != null + + val direction = if (isSignedIn) { + TabsTrayFragmentDirections.actionGlobalAccountSettingsFragment() + } else { + TabsTrayFragmentDirections.actionGlobalTurnOnSync() + } + navController.navigateBlockingForAsyncNavGraph(direction) + } + + override fun onTabSettingsClicked() { + navController.navigateBlockingForAsyncNavGraph( + TabsTrayFragmentDirections.actionGlobalTabSettingsFragment() + ) + } + + override fun onOpenRecentlyClosedClicked() { + navController.navigateBlockingForAsyncNavGraph( + TabsTrayFragmentDirections.actionGlobalRecentlyClosed() + ) + metrics.track(Event.RecentlyClosedTabsOpened) + } + + override fun onShareTabs(tabs: Collection) { + val data = tabs.map { + ShareData(url = it.url, title = it.title) + } + val directions = TabsTrayFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + navController.navigateBlockingForAsyncNavGraph(directions) + } + + override fun onShareTabsOfTypeClicked(private: Boolean) { + val tabs = browserStore.state.getNormalOrPrivateTabs(private) + val data = tabs.map { + ShareData(url = it.content.url, title = it.content.title) + } + val directions = TabsTrayFragmentDirections.actionGlobalShareFragment( + data = data.toTypedArray() + ) + navController.navigateBlockingForAsyncNavGraph(directions) + } + + @OptIn(ExperimentalCoroutinesApi::class) + override fun onCloseAllTabsClicked(private: Boolean) { + val sessionsToClose = if (private) { + HomeFragment.ALL_PRIVATE_TABS + } else { + HomeFragment.ALL_NORMAL_TABS + } + + dismissTabTrayAndNavigateHome(sessionsToClose) + } + + override fun onSaveToCollections(tabs: Collection) { + metrics.track(Event.TabsTraySaveToCollectionPressed) + + CollectionsDialog( + storage = collectionStorage, + onPositiveButtonClick = { existingCollection -> + tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode) + + // If collection is null, a new one was created. + val event = if (existingCollection == null) { + Event.CollectionSaved(browserStore.state.normalTabs.size, tabs.size) + } else { + Event.CollectionTabsAdded(browserStore.state.normalTabs.size, tabs.size) + } + metrics.track(event) + + browserStore.getTabSessionState(tabs) + }, + onNegativeButtonClick = { + tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode) + } + ).show(context) + } + + override fun onSaveToBookmarks(tabs: Collection) { + tabs.forEach { tab -> + // We don't combine the context with lifecycleScope so that our jobs are not cancelled + // if we leave the fragment, i.e. we still want the bookmarks to be added. + CoroutineScope(Dispatchers.IO).launch { + bookmarksUseCase.addBookmark(tab.url, tab.title) + } + } + + tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode) + + // TODO show successful snackbar here (regardless of operation success). + } + + override fun onSyncedTabClicked(tab: SyncTab) { + metrics.track(Event.SyncedTabOpened) + + dismissTabTray() + activity.openToBrowserAndLoad( + searchTermOrURL = tab.active().url, + newTab = true, + from = BrowserDirection.FromTabTray + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt new file mode 100644 index 000000000..e6b833c7d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabCounterBinding.kt @@ -0,0 +1,44 @@ +/* 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.tabstray + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import mozilla.components.ui.tabcounter.TabCounter + +/** + * Updates the tab counter to the size of [BrowserState.normalTabs]. + */ +class TabCounterBinding( + private val store: BrowserStore, + private val counter: TabCounter +) : LifecycleAwareFeature { + + private var scope: CoroutineScope? = null + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + scope = store.flowScoped { flow -> + flow.map { it.normalTabs } + .ifChanged() + .collect { + counter.setCount(it.size) + } + } + } + + override fun stop() { + scope?.cancel() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt new file mode 100644 index 000000000..8e27666f7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabLayoutMediator.kt @@ -0,0 +1,92 @@ +/* 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.tabstray + +import androidx.annotation.VisibleForTesting +import com.google.android.material.tabs.TabLayout +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.base.feature.LifecycleAwareFeature +import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_NORMAL_TABS +import org.mozilla.fenix.tabstray.TrayPagerAdapter.Companion.POSITION_PRIVATE_TABS +import org.mozilla.fenix.utils.Do + +/** + * Selected the selected pager depending on the [BrowserStore] state and synchronizes user actions + * with the pager position. + */ +class TabLayoutMediator( + private val tabLayout: TabLayout, + interactor: TabsTrayInteractor, + private val browsingModeManager: BrowsingModeManager, + private val tabsTrayStore: TabsTrayStore, + metrics: MetricController +) : LifecycleAwareFeature { + + private val observer = TabLayoutObserver(interactor, metrics) + + /** + * Start observing the [TabLayout] and select the current tab for initial state. + */ + override fun start() { + tabLayout.addOnTabSelectedListener(observer) + + selectActivePage() + } + + override fun stop() { + tabLayout.removeOnTabSelectedListener(observer) + } + + @VisibleForTesting + internal fun selectActivePage() { + val selectedPagerPosition = + when (browsingModeManager.mode.isPrivate) { + true -> POSITION_PRIVATE_TABS + false -> POSITION_NORMAL_TABS + } + + selectTabAtPosition(selectedPagerPosition) + } + + fun selectTabAtPosition(position: Int) { + tabLayout.getTabAt(position)?.select() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position))) + } +} + +/** + * An observer for the [TabLayout] used for the Tabs Tray. + */ +internal class TabLayoutObserver( + private val interactor: TabsTrayInteractor, + private val metrics: MetricController +) : TabLayout.OnTabSelectedListener { + + private var initialScroll = true + + override fun onTabSelected(tab: TabLayout.Tab) { + // Do not animate the initial scroll when opening the tabs tray. + val animate = if (initialScroll) { + initialScroll = false + false + } else { + true + } + + interactor.setCurrentTrayPosition(tab.position, animate) + + Do exhaustive when (Page.positionToPage(tab.position)) { + Page.NormalTabs -> metrics.track(Event.TabsTrayNormalModeTapped) + Page.PrivateTabs -> metrics.track(Event.TabsTrayPrivateModeTapped) + Page.SyncedTabs -> metrics.track(Event.TabsTraySyncedModeTapped) + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) = Unit + override fun onTabReselected(tab: TabLayout.Tab) = Unit +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt new file mode 100644 index 000000000..0999ad47e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayController.kt @@ -0,0 +1,59 @@ +/* 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.tabstray + +import androidx.navigation.NavController +import kotlinx.coroutines.CoroutineScope +import mozilla.components.concept.base.profiler.Profiler +import mozilla.components.service.fxa.manager.FxaAccountManager +import org.mozilla.fenix.browser.browsingmode.BrowsingMode +import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections + +interface TabsTrayController { + + /** + * Called when user clicks the new tab button. + */ + fun onNewTabTapped(isPrivate: Boolean) +} + +class DefaultTabsTrayController( + private val store: TabsTrayStore, + private val browsingModeManager: BrowsingModeManager, + private val navController: NavController, + private val profiler: Profiler?, + private val navigationInteractor: NavigationInteractor, + private val metrics: MetricController, + private val ioScope: CoroutineScope, + private val accountManager: FxaAccountManager +) : TabsTrayController { + + override fun onNewTabTapped(isPrivate: Boolean) { + val startTime = profiler?.getProfilerTime() + browsingModeManager.mode = BrowsingMode.fromBoolean(isPrivate) + navController.navigateBlockingForAsyncNavGraph( + TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) + navigationInteractor.onTabTrayDismissed() + profiler?.addMarker( + "DefaultTabTrayController.onNewTabTapped", + startTime + ) + sendNewTabEvent(isPrivate) + } + + private fun sendNewTabEvent(isPrivateModeSelected: Boolean) { + val eventToSend = if (isPrivateModeSelected) { + Event.NewPrivateTabTapped + } else { + Event.NewTabTapped + } + + metrics.track(eventToSend) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt new file mode 100644 index 000000000..18e9af8f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayDialog.kt @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabstray + +import android.app.Dialog +import android.content.Context +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor + +/** + * Default tabs tray dialog implementation for overriding the default on back pressed. + */ +class TabsTrayDialog( + context: Context, + theme: Int, + private val interactor: () -> BrowserTrayInteractor +) : Dialog(context, theme) { + override fun onBackPressed() { + if (interactor.invoke().onBackPressed()) { + return + } + + dismiss() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt index b0a0fc12a..bae954a3b 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayFragment.kt @@ -5,28 +5,80 @@ package org.mozilla.fenix.tabstray import android.content.Context +import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.app.AppCompatDialogFragment import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.tabs.TabLayout +import kotlinx.android.synthetic.main.component_tabstray.view.* import kotlinx.android.synthetic.main.component_tabstray2.* import kotlinx.android.synthetic.main.component_tabstray2.view.* +import kotlinx.android.synthetic.main.component_tabstray2.view.tab_tray_overflow +import kotlinx.android.synthetic.main.component_tabstray2.view.tab_wrapper +import kotlinx.android.synthetic.main.component_tabstray_fab.* +import kotlinx.android.synthetic.main.fragment_tab_tray_dialog.* +import kotlinx.android.synthetic.main.tabs_tray_tab_counter2.* +import kotlinx.android.synthetic.main.tabstray_multiselect_items.* +import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.plus +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.concept.tabstray.Tab +import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import org.mozilla.fenix.HomeActivity +import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R +import org.mozilla.fenix.components.StoreProvider +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.home.HomeScreenViewModel +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor +import org.mozilla.fenix.tabstray.browser.DefaultBrowserTrayInteractor +import org.mozilla.fenix.tabstray.browser.SelectionHandleBinding +import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding +import org.mozilla.fenix.tabstray.browser.SelectionBannerBinding.VisibilityModifier +import org.mozilla.fenix.tabstray.ext.getTrayPosition +import org.mozilla.fenix.tabstray.ext.showWithTheme +import org.mozilla.fenix.utils.allowUndo +import kotlin.math.max +@Suppress("TooManyFunctions", "LargeClass") class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { - lateinit var behavior: BottomSheetBehavior + private var fabView: View? = null + private lateinit var tabsTrayStore: TabsTrayStore + private lateinit var browserTrayInteractor: BrowserTrayInteractor + private lateinit var tabsTrayController: DefaultTabsTrayController + private lateinit var behavior: BottomSheetBehavior + + private val tabLayoutMediator = ViewBoundFeatureWrapper() + private val tabCounterBinding = ViewBoundFeatureWrapper() + private val floatingActionButtonBinding = ViewBoundFeatureWrapper() + private val selectionBannerBinding = ViewBoundFeatureWrapper() + private val selectionHandleBinding = ViewBoundFeatureWrapper() + private val tabsTrayCtaBinding = ViewBoundFeatureWrapper() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NO_TITLE, R.style.TabTrayDialogStyle) } + override fun onCreateDialog(savedInstanceState: Bundle?) = + TabsTrayDialog(requireContext(), theme) { browserTrayInteractor } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -38,21 +90,171 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { behavior = BottomSheetBehavior.from(view.tab_wrapper) + tabsTrayStore = StoreProvider.get(this) { TabsTrayStore() } + + fabView = LayoutInflater.from(containerView.context) + .inflate(R.layout.component_tabstray_fab, containerView, true) + return containerView } + @Suppress("LongMethod") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val activity = activity as HomeActivity + + requireComponents.analytics.metrics.track(Event.TabsTrayOpened) + + val navigationInteractor = + DefaultNavigationInteractor( + context = requireContext(), + activity = activity, + tabsTrayStore = tabsTrayStore, + browserStore = requireComponents.core.store, + navController = findNavController(), + metrics = requireComponents.analytics.metrics, + dismissTabTray = ::dismissTabsTray, + dismissTabTrayAndNavigateHome = ::dismissTabsTrayAndNavigateHome, + bookmarksUseCase = requireComponents.useCases.bookmarksUseCases, + collectionStorage = requireComponents.core.tabCollectionStorage, + accountManager = requireComponents.backgroundServices.accountManager + ) + + tabsTrayController = DefaultTabsTrayController( + store = tabsTrayStore, + browsingModeManager = activity.browsingModeManager, + navController = findNavController(), + navigationInteractor = navigationInteractor, + profiler = requireComponents.core.engine.profiler, + accountManager = requireComponents.backgroundServices.accountManager, + metrics = requireComponents.analytics.metrics, + ioScope = lifecycleScope + Dispatchers.IO + ) + + browserTrayInteractor = DefaultBrowserTrayInteractor( + tabsTrayStore, + this@TabsTrayFragment, + tabsTrayController, + requireComponents.useCases.tabsUseCases.selectTab, + requireComponents.settings, + requireComponents.analytics.metrics + ) + + setupMenu(view, navigationInteractor) + setupPager( + view.context, + tabsTrayStore, + this, + browserTrayInteractor, + navigationInteractor + ) + + setupBackgroundDismissalListener { + requireComponents.analytics.metrics.track(Event.TabsTrayClosed) + dismissAllowingStateLoss() + } - setupPager(view.context, this) + behavior.setUpTrayBehavior( + isLandscape = requireContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE, + maxNumberOfTabs = max( + requireContext().components.core.store.state.normalTabs.size, + requireContext().components.core.store.state.privateTabs.size + ), + numberForExpandingTray = if (requireContext().settings().gridTabView) { + EXPAND_AT_GRID_SIZE + } else { + EXPAND_AT_LIST_SIZE + }, + navigationInteractor = navigationInteractor + ) + + tabsTrayCtaBinding.set( + feature = TabsTrayInfoBannerBinding( + context = view.context, + store = requireComponents.core.store, + infoBannerView = view.info_banner, + settings = requireComponents.settings, + navigationInteractor = navigationInteractor, + metrics = requireComponents.analytics.metrics + ), + owner = this, + view = view + ) + + tabLayoutMediator.set( + feature = TabLayoutMediator( + tabLayout = tab_layout, + interactor = this, + browsingModeManager = activity.browsingModeManager, + tabsTrayStore = tabsTrayStore, + metrics = requireComponents.analytics.metrics + ), owner = this, + view = view + ) + + tabCounterBinding.set( + feature = TabCounterBinding( + store = requireComponents.core.store, + counter = tab_counter + ), + owner = this, + view = view + ) + + floatingActionButtonBinding.set( + feature = FloatingActionButtonBinding( + store = tabsTrayStore, + actionButton = new_tab_button, + browserTrayInteractor = browserTrayInteractor + ), + owner = this, + view = view + ) + + selectionBannerBinding.set( + feature = SelectionBannerBinding( + context = requireContext(), + store = tabsTrayStore, + navInteractor = navigationInteractor, + tabsTrayInteractor = this, + containerView = view, + backgroundView = topBar, + showOnSelectViews = VisibilityModifier( + collect_multi_select, + share_multi_select, + menu_multi_select, + multiselect_title, + exit_multi_select + ), + showOnNormalViews = VisibilityModifier( + tab_layout, + tab_tray_overflow, + new_tab_button + ) + ), + owner = this, + view = view + ) + + selectionHandleBinding.set( + feature = SelectionHandleBinding( + store = tabsTrayStore, + handle = handle, + containerLayout = tab_wrapper + ), + owner = this, + view = view + ) } - override fun setCurrentTrayPosition(position: Int) { - tabsTray.currentItem = position + override fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) { + tabsTray.setCurrentItem(position, smoothScroll) + tab_layout.getTabAt(position)?.select() + tabsTrayStore.dispatch(TabsTrayAction.PageSelected(Page.positionToPage(position))) } override fun navigateToBrowser() { - dismissAllowingStateLoss() + dismissTabsTray() val navController = findNavController() @@ -61,36 +263,115 @@ class TabsTrayFragment : AppCompatDialogFragment(), TabsTrayInteractor { } if (!navController.popBackStack(R.id.browserFragment, false)) { - navController.navigate(R.id.browserFragment) + navController.navigateBlockingForAsyncNavGraph(R.id.browserFragment) } } - override fun tabRemoved(sessionId: String) { - // TODO re-implement these methods - // showUndoSnackbarForTab(sessionId) - // removeIfNotLastTab(sessionId) + override fun onDeleteTab(tabId: String) { + val browserStore = requireComponents.core.store + val tab = browserStore.state.findTab(tabId) + + tab?.let { + if (browserStore.state.getNormalOrPrivateTabs(it.content.private).size != 1) { + requireComponents.useCases.tabsUseCases.removeTab(tabId) + showUndoSnackbarForTab(it) + } else { + dismissTabsTrayAndNavigateHome(tabId) + } + } } - private fun setupPager(context: Context, interactor: TabsTrayInteractor) { + override fun onDeleteTabs(tabs: Collection) { + tabs.forEach { + onDeleteTab(it.id) + } + } + + private fun showUndoSnackbarForTab(removedTab: TabSessionState) { + val snackbarMessage = + when (removedTab.content.private) { + true -> getString(R.string.snackbar_private_tab_closed) + false -> getString(R.string.snackbar_tab_closed) + } + + lifecycleScope.allowUndo( + requireView(), + snackbarMessage, + getString(R.string.snackbar_deleted_undo), + { + requireComponents.useCases.tabsUseCases.undo.invoke() + tabLayoutMediator.withFeature { it.selectTabAtPosition(removedTab.getTrayPosition()) } + }, + operation = { }, + elevation = ELEVATION, + anchorView = new_tab_button + ) + } + + private fun setupPager( + context: Context, + store: TabsTrayStore, + trayInteractor: TabsTrayInteractor, + browserInteractor: BrowserTrayInteractor, + navigationInteractor: NavigationInteractor + ) { tabsTray.apply { - adapter = TrayPagerAdapter(context, interactor) + adapter = TrayPagerAdapter( + context, + store, + browserInteractor, + navigationInteractor, + trayInteractor, + requireComponents.core.store + ) isUserInputEnabled = false } + } + + private fun setupMenu(view: View, navigationInteractor: NavigationInteractor) { + view.tab_tray_overflow.setOnClickListener { anchor -> + + requireComponents.analytics.metrics.track(Event.TabsTrayMenuOpened) + + val menu = MenuIntegration( + context = requireContext(), + browserStore = requireComponents.core.store, + tabsTrayStore = tabsTrayStore, + tabLayout = tab_layout, + navigationInteractor = navigationInteractor + ).build() + + menu.showWithTheme(anchor) + } + } - tab_layout.addOnTabSelectedListener(TabLayoutObserver(interactor)) + private fun setupBackgroundDismissalListener(block: (View) -> Unit) { + tabLayout.setOnClickListener(block) + handle.setOnClickListener(block) + } + + private val homeViewModel: HomeScreenViewModel by activityViewModels() + + private fun dismissTabsTrayAndNavigateHome(sessionId: String) { + homeViewModel.sessionToDelete = sessionId + val directions = NavGraphDirections.actionGlobalHome() + findNavController().navigateBlockingForAsyncNavGraph(directions) + dismissTabsTray() } -} -/** - * An observer for the [TabLayout] used for the Tabs Tray. - */ -internal class TabLayoutObserver( - private val interactor: TabsTrayInteractor -) : TabLayout.OnTabSelectedListener { - override fun onTabSelected(tab: TabLayout.Tab) { - interactor.setCurrentTrayPosition(tab.position) + private fun dismissTabsTray() { + dismissAllowingStateLoss() + requireComponents.analytics.metrics.track(Event.TabsTrayClosed) } - override fun onTabUnselected(tab: TabLayout.Tab) = Unit - override fun onTabReselected(tab: TabLayout.Tab) = Unit + companion object { + // Minimum number of list items for which to show the tabs tray as expanded. + const val EXPAND_AT_LIST_SIZE = 4 + + // Minimum number of grid items for which to show the tabs tray as expanded. + private const val EXPAND_AT_GRID_SIZE = 3 + + // Elevation for undo toasts + private const val ELEVATION = 80f + } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt new file mode 100644 index 000000000..a1251f37e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInfoBannerBinding.kt @@ -0,0 +1,126 @@ +/* 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.tabstray + +import android.content.Context +import android.view.View.VISIBLE +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import kotlin.math.max +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.R +import org.mozilla.fenix.browser.infobanner.InfoBanner +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.utils.Settings + +class TabsTrayInfoBannerBinding( + private val context: Context, + private val store: BrowserStore, + private val infoBannerView: ViewGroup, + private val settings: Settings, + private val navigationInteractor: NavigationInteractor, + private val metrics: MetricController? +) : LifecycleAwareFeature { + private var scope: CoroutineScope? = null + + @VisibleForTesting + internal var banner: InfoBanner? = null + + @ExperimentalCoroutinesApi + override fun start() { + scope = store.flowScoped { flow -> + flow.map { state -> max(state.normalTabs.size, state.privateTabs.size) } + .ifChanged() + .collect { tabCount -> + if (tabCount >= TAB_COUNT_SHOW_CFR) { + displayInfoBannerIfNeeded(settings) + } + } + } + } + + override fun stop() { + scope?.cancel() + } + + private fun displayInfoBannerIfNeeded(settings: Settings) { + banner = displayGridViewBannerIfNeeded(settings) + ?: displayAutoCloseTabsBannerIfNeeded(settings) + + banner?.apply { + infoBannerView.visibility = VISIBLE + showBanner() + } + } + + private fun displayGridViewBannerIfNeeded(settings: Settings): InfoBanner? { + return if ( + settings.shouldShowGridViewBanner && + settings.canShowCfr && + settings.listTabView + ) { + InfoBanner( + context = context, + message = context.getString(R.string.tab_tray_grid_view_banner_message), + dismissText = context.getString(R.string.tab_tray_grid_view_banner_negative_button_text), + actionText = context.getString(R.string.tab_tray_grid_view_banner_positive_button_text), + container = infoBannerView, + dismissByHiding = true, + dismissAction = { + metrics?.track(Event.TabsTrayCfrDismissed) + settings.shouldShowGridViewBanner = false + } + ) { + navigationInteractor.onTabSettingsClicked() + metrics?.track(Event.TabsTrayCfrTapped) + settings.shouldShowGridViewBanner = false + } + } else { + null + } + } + + private fun displayAutoCloseTabsBannerIfNeeded(settings: Settings): InfoBanner? { + return if ( + settings.shouldShowAutoCloseTabsBanner && + settings.canShowCfr + ) { + InfoBanner( + context = context, + message = context.getString(R.string.tab_tray_close_tabs_banner_message), + dismissText = context.getString(R.string.tab_tray_close_tabs_banner_negative_button_text), + actionText = context.getString(R.string.tab_tray_close_tabs_banner_positive_button_text), + container = infoBannerView, + dismissByHiding = true, + dismissAction = { + metrics?.track(Event.TabsTrayCfrDismissed) + settings.shouldShowAutoCloseTabsBanner = false + } + ) { + navigationInteractor.onTabSettingsClicked() + metrics?.track(Event.TabsTrayCfrTapped) + settings.shouldShowAutoCloseTabsBanner = false + } + } else { + null + } + } + + companion object { + @VisibleForTesting + internal const val TAB_COUNT_SHOW_CFR = 6 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt index 6c479aad3..5c3a7371a 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayInteractor.kt @@ -4,11 +4,16 @@ package org.mozilla.fenix.tabstray +import mozilla.components.concept.tabstray.Tab + interface TabsTrayInteractor { /** * Set the current tray item to the clamped [position]. + * + * @param position The position on the tray to focus. + * @param smoothScroll If true, animate the scrolling from the current tab to [position]. */ - fun setCurrentTrayPosition(position: Int) + fun setCurrentTrayPosition(position: Int, smoothScroll: Boolean) /** * Dismisses the tabs tray and navigates to the browser. @@ -16,7 +21,12 @@ interface TabsTrayInteractor { fun navigateToBrowser() /** - * Invoked when a tab is removed from the tabs tray with the given [sessionId]. + * Invoked when a tab is removed from the tabs tray with the given [tabId]. + */ + fun onDeleteTab(tabId: String) + + /** + * Invoked when [Tab]s need to be deleted. */ - fun tabRemoved(sessionId: String) + fun onDeleteTabs(tabs: Collection) } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMenu.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMenu.kt new file mode 100644 index 000000000..0db845931 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayMenu.kt @@ -0,0 +1,100 @@ +/* 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.tabstray + +import android.content.Context +import com.google.android.material.tabs.TabLayout +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.tabstray.ext.isNormalModeSelected +import org.mozilla.fenix.tabstray.ext.isPrivateModeSelected +import org.mozilla.fenix.tabstray.ext.isSyncedModeSelected + +class TabsTrayMenu( + private val context: Context, + browserStore: BrowserStore, + private val tabLayout: TabLayout, + private val onItemTapped: (Item) -> Unit = {} +) { + + private val checkOpenTabs = + when { + tabLayout.isNormalModeSelected() -> + browserStore.state.normalTabs.isNotEmpty() + tabLayout.isPrivateModeSelected() -> + browserStore.state.privateTabs.isNotEmpty() + else -> + false + } + + private val shouldShowSelectOrShare = { tabLayout.isNormalModeSelected() && checkOpenTabs } + private val shouldShowTabSetting = { !tabLayout.isSyncedModeSelected() } + private val shouldShowAccountSetting = { tabLayout.isSyncedModeSelected() } + + sealed class Item { + object ShareAllTabs : Item() + object OpenAccountSettings : Item() + object OpenTabSettings : Item() + object SelectTabs : Item() + object CloseAllTabs : Item() + object OpenRecentlyClosed : Item() + } + + val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } + + private val menuItems by lazy { + listOf( + SimpleBrowserMenuItem( + context.getString(R.string.tabs_tray_select_tabs), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.SelectTabs) + }.apply { visible = shouldShowSelectOrShare }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_item_share), + textColorResource = R.color.primary_text_normal_theme + ) { + context.components.analytics.metrics.track(Event.TabsTrayShareAllTabsPressed) + onItemTapped.invoke(Item.ShareAllTabs) + }.apply { visible = shouldShowSelectOrShare }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_account_settings), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.OpenAccountSettings) + }.apply { visible = shouldShowAccountSetting }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_tab_settings), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.OpenTabSettings) + }.apply { visible = shouldShowTabSetting }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_recently_closed), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.OpenRecentlyClosed) + }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_menu_item_close), + textColorResource = R.color.primary_text_normal_theme + ) { + context.components.analytics.metrics.track(Event.TabsTrayCloseAllTabsPressed) + onItemTapped.invoke(Item.CloseAllTabs) + }.apply { visible = { checkOpenTabs } } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt new file mode 100644 index 000000000..28c9da2f2 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayStore.kt @@ -0,0 +1,167 @@ +/* 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.tabstray + +import mozilla.components.concept.tabstray.Tab +import mozilla.components.lib.state.Action +import mozilla.components.lib.state.Middleware +import mozilla.components.lib.state.State +import mozilla.components.lib.state.Store + +/** + * Value type that represents the state of the tabs tray. + * + * @property selectedPage The current page in the tray can be on. + * @property mode Whether the browser tab list is in multi-select mode or not with the set of + * currently selected tabs. + * @property syncing Whether the Synced Tabs feature should fetch the latest tabs from paired + * devices. + */ +data class TabsTrayState( + val selectedPage: Page = Page.NormalTabs, + val mode: Mode = Mode.Normal, + val syncing: Boolean = false +) : State { + + /** + * The current mode that the tabs list is in. + */ + sealed class Mode { + + /** + * A set of selected tabs which we would want to perform an action on. + */ + open val selectedTabs = emptySet() + + /** + * The default mode the tabs list is in. + */ + object Normal : Mode() + + /** + * The multi-select mode that the tabs list is in containing the set of currently + * selected tabs. + */ + data class Select(override val selectedTabs: Set) : Mode() + } +} + +/** + * The different pagers in the tray that we can switch between in the [TrayPagerAdapter]. + */ +enum class Page { + + /** + * The pager position that displays normal tabs. + */ + NormalTabs, + + /** + * The pager position that displays private tabs. + */ + PrivateTabs, + + /** + * The pager position that displays Synced Tabs. + */ + SyncedTabs; + + companion object { + fun positionToPage(position: Int): Page { + return when (position) { + 0 -> NormalTabs + 1 -> PrivateTabs + else -> SyncedTabs + } + } + } +} + +/** + * [Action] implementation related to [TabsTrayStore]. + */ +sealed class TabsTrayAction : Action { + + /** + * Entered multi-select mode. + */ + object EnterSelectMode : TabsTrayAction() + + /** + * Exited multi-select mode. + */ + object ExitSelectMode : TabsTrayAction() + + /** + * Added a new [Tab] to the selection set. + */ + data class AddSelectTab(val tab: Tab) : TabsTrayAction() + + /** + * Removed a [Tab] from the selection set. + */ + data class RemoveSelectTab(val tab: Tab) : TabsTrayAction() + + /** + * The active page in the tray that is now in focus. + */ + data class PageSelected(val page: Page) : TabsTrayAction() + + /** + * A request to perform a "sync" action. + */ + object SyncNow : TabsTrayAction() + + /** + * When a "sync" action has completed; this can be triggered immediately after [SyncNow] if + * no sync action was able to be performed. + */ + object SyncCompleted : TabsTrayAction() +} + +/** + * Reducer for [TabsTrayStore]. + */ +internal object TabsTrayReducer { + fun reduce(state: TabsTrayState, action: TabsTrayAction): TabsTrayState { + return when (action) { + is TabsTrayAction.EnterSelectMode -> + state.copy(mode = TabsTrayState.Mode.Select(emptySet())) + is TabsTrayAction.ExitSelectMode -> + state.copy(mode = TabsTrayState.Mode.Normal) + is TabsTrayAction.AddSelectTab -> + state.copy(mode = TabsTrayState.Mode.Select(state.mode.selectedTabs + action.tab)) + is TabsTrayAction.RemoveSelectTab -> { + val selected = state.mode.selectedTabs - action.tab + state.copy( + mode = if (selected.isEmpty()) { + TabsTrayState.Mode.Normal + } else { + TabsTrayState.Mode.Select(selected) + } + ) + } + is TabsTrayAction.PageSelected -> + state.copy(selectedPage = action.page) + is TabsTrayAction.SyncNow -> + state.copy(syncing = true) + is TabsTrayAction.SyncCompleted -> + state.copy(syncing = false) + } + } +} + +/** + * A [Store] that holds the [TabsTrayState] for the tabs tray and reduces [TabsTrayAction]s + * dispatched to the store. + */ +class TabsTrayStore( + initialState: TabsTrayState = TabsTrayState(), + middlewares: List> = emptyList() +) : Store( + initialState, + TabsTrayReducer::reduce, + middlewares +) diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt new file mode 100644 index 000000000..e0c443526 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TabsTrayViewHolder.kt @@ -0,0 +1,231 @@ +/* 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.tabstray + +import android.view.View +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageButton +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.checkbox_item.view.* +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.browser.tabstray.TabViewHolder +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.browser.tabstray.thumbnail.TabThumbnailView +import mozilla.components.browser.toolbar.MAX_URI_LENGTH +import mozilla.components.concept.base.images.ImageLoadRequest +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.concept.engine.mediasession.MediaSession +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import org.mozilla.fenix.R +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.increaseTapArea +import org.mozilla.fenix.ext.removeAndDisable +import org.mozilla.fenix.ext.removeTouchDelegate +import org.mozilla.fenix.ext.showAndEnable +import org.mozilla.fenix.ext.toShortUrl +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.selection.SelectionInteractor +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor +import org.mozilla.fenix.tabstray.ext.isSelect + +/** + * A RecyclerView ViewHolder implementation for "tab" items. + */ +abstract class TabsTrayViewHolder( + itemView: View, + private val imageLoader: ImageLoader, + private val trayStore: TabsTrayStore, + private val selectionHolder: SelectionHolder?, + private val store: BrowserStore = itemView.context.components.core.store, + private val metrics: MetricController = itemView.context.components.analytics.metrics +) : TabViewHolder(itemView) { + + private val faviconView: ImageView? = + itemView.findViewById(R.id.mozac_browser_tabstray_favicon_icon) + private val titleView: TextView = itemView.findViewById(R.id.mozac_browser_tabstray_title) + private val closeView: AppCompatImageButton = + itemView.findViewById(R.id.mozac_browser_tabstray_close) + private val thumbnailView: TabThumbnailView = + itemView.findViewById(R.id.mozac_browser_tabstray_thumbnail) + + @VisibleForTesting + internal val urlView: TextView? = itemView.findViewById(R.id.mozac_browser_tabstray_url) + private val playPauseButtonView: ImageButton = itemView.findViewById(R.id.play_pause_button) + + abstract val browserTrayInteractor: BrowserTrayInteractor + abstract val thumbnailSize: Int + + override var tab: Tab? = null + + /** + * Displays the data of the given session and notifies the given observable about events. + */ + @Suppress("ComplexMethod", "LongMethod") + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { + this.tab = tab + + updateTitle(tab) + updateUrl(tab) + updateFavicon(tab) + updateCloseButtonDescription(tab.title) + updateSelectedTabIndicator(isSelected) + updateMediaState(tab) + + if (selectionHolder != null) { + setSelectionInteractor(tab, selectionHolder, browserTrayInteractor) + } else { + itemView.setOnClickListener { browserTrayInteractor.open(tab) } + } + + if (tab.thumbnail != null) { + thumbnailView.setImageBitmap(tab.thumbnail) + } else { + loadIntoThumbnailView(thumbnailView, tab.id) + } + } + + fun showTabIsMultiSelectEnabled(isSelected: Boolean) { + itemView.selected_mask.isVisible = isSelected + closeView.isInvisible = trayStore.state.mode is TabsTrayState.Mode.Select + } + + private fun updateFavicon(tab: Tab) { + if (tab.icon != null) { + faviconView?.visibility = View.VISIBLE + faviconView?.setImageBitmap(tab.icon) + } else { + faviconView?.visibility = View.GONE + } + } + + private fun updateTitle(tab: Tab) { + val title = if (tab.title.isNotEmpty()) { + tab.title + } else { + tab.url + } + titleView.text = title + } + + private fun updateUrl(tab: Tab) { + // Truncate to MAX_URI_LENGTH to prevent the UI from locking up for + // extremely large URLs such as data URIs or bookmarklets. The same + // is done in the toolbar and awesomebar: + // https://github.com/mozilla-mobile/fenix/issues/1824 + // https://github.com/mozilla-mobile/android-components/issues/6985 + urlView?.text = tab.url + .toShortUrl(itemView.context.components.publicSuffixList) + .take(MAX_URI_LENGTH) + } + + private fun updateCloseButtonDescription(title: String) { + closeView.contentDescription = + closeView.context.getString(R.string.close_tab_title, title) + } + + /** + * NB: Why do we query for the media state from the store, when we have [Tab.playbackState] and + * [Tab.controller] already mapped? + */ + private fun updateMediaState(tab: Tab) { + // Media state + playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS) + + with(playPauseButtonView) { + invalidate() + val sessionState = store.state.findTabOrCustomTab(tab.id) + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PAUSED -> { + showAndEnable() + contentDescription = + context.getString(R.string.mozac_feature_media_notification_action_play) + setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.media_state_play) + ) + } + + MediaSession.PlaybackState.PLAYING -> { + showAndEnable() + contentDescription = + context.getString(R.string.mozac_feature_media_notification_action_pause) + setImageDrawable( + AppCompatResources.getDrawable(context, R.drawable.media_state_pause) + ) + } + + else -> { + removeTouchDelegate() + removeAndDisable() + } + } + + setOnClickListener { + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PLAYING -> { + metrics.track(Event.TabMediaPause) + sessionState.mediaSessionState?.controller?.pause() + } + + MediaSession.PlaybackState.PAUSED -> { + metrics.track(Event.TabMediaPlay) + sessionState.mediaSessionState?.controller?.play() + } + else -> throw AssertionError( + "Play/Pause button clicked without play/pause state." + ) + } + } + } + } + + private fun loadIntoThumbnailView(thumbnailView: ImageView, id: String) { + imageLoader.loadIntoView(thumbnailView, ImageLoadRequest(id, thumbnailSize)) + } + + private fun setSelectionInteractor( + item: Tab, + holder: SelectionHolder, + interactor: SelectionInteractor + ) { + itemView.setOnClickListener { + val selected = holder.selectedItems + when { + selected.isEmpty() && trayStore.state.mode.isSelect().not() -> interactor.open(item) + item in selected -> interactor.deselect(item) + else -> interactor.select(item) + } + } + + itemView.setOnLongClickListener { + if (holder.selectedItems.isEmpty()) { + metrics.track(Event.CollectionTabLongPressed) + interactor.select(item) + true + } else { + false + } + } + } + + companion object { + internal const val PLAY_PAUSE_BUTTON_EXTRA_DPS = 24 + internal const val GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS = 24 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt index c4ea545ae..7b72b8144 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TrayPagerAdapter.kt @@ -8,42 +8,79 @@ import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_NORMAL_TAB -import org.mozilla.fenix.tabstray.BrowserTabViewHolder.Companion.LAYOUT_ID_PRIVATE_TAB -import org.mozilla.fenix.tabtray.FenixTabsAdapter +import mozilla.components.browser.state.selector.normalTabs +import mozilla.components.browser.state.selector.privateTabs +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.store.BrowserStore +import org.mozilla.fenix.sync.SyncedTabsAdapter +import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter +import org.mozilla.fenix.tabstray.browser.BrowserTrayInteractor +import org.mozilla.fenix.tabstray.syncedtabs.TabClickDelegate +import org.mozilla.fenix.tabstray.viewholders.AbstractTrayViewHolder +import org.mozilla.fenix.tabstray.viewholders.NormalBrowserTabViewHolder +import org.mozilla.fenix.tabstray.viewholders.PrivateBrowserTabViewHolder +import org.mozilla.fenix.tabstray.viewholders.SyncedTabViewHolder class TrayPagerAdapter( - context: Context, - val interactor: TabsTrayInteractor -) : RecyclerView.Adapter() { + private val context: Context, + private val store: TabsTrayStore, + private val browserInteractor: BrowserTrayInteractor, + private val navInteractor: NavigationInteractor, + private val interactor: TabsTrayInteractor, + private val browserStore: BrowserStore +) : RecyclerView.Adapter() { - private val normalAdapter by lazy { FenixTabsAdapter(context) } - private val privateAdapter by lazy { FenixTabsAdapter(context) } + private val normalAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) } + private val privateAdapter by lazy { BrowserTabsAdapter(context, browserInteractor, store) } + private val syncedTabsAdapter by lazy { SyncedTabsAdapter(TabClickDelegate(navInteractor)) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrayViewHolder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AbstractTrayViewHolder { val itemView = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + val selectedTab = browserStore.state.selectedTab + return when (viewType) { - LAYOUT_ID_NORMAL_TAB -> BrowserTabViewHolder(itemView, interactor) - LAYOUT_ID_PRIVATE_TAB -> BrowserTabViewHolder(itemView, interactor) + NormalBrowserTabViewHolder.LAYOUT_ID -> { + NormalBrowserTabViewHolder( + itemView, + store, + interactor, + browserStore.state.normalTabs.indexOf(selectedTab) + ) + } + PrivateBrowserTabViewHolder.LAYOUT_ID -> { + PrivateBrowserTabViewHolder( + itemView, + store, + interactor, + browserStore.state.privateTabs.indexOf(selectedTab) + ) + } + SyncedTabViewHolder.LAYOUT_ID -> { + SyncedTabViewHolder( + itemView, + store + ) + } else -> throw IllegalStateException("Unknown viewType.") } } - override fun onBindViewHolder(viewHolder: TrayViewHolder, position: Int) { + override fun onBindViewHolder(viewHolder: AbstractTrayViewHolder, position: Int) { val adapter = when (position) { POSITION_NORMAL_TABS -> normalAdapter POSITION_PRIVATE_TABS -> privateAdapter + POSITION_SYNCED_TABS -> syncedTabsAdapter else -> throw IllegalStateException("View type does not exist.") } - - viewHolder.bind(adapter) + viewHolder.bind(adapter, browserInteractor.getLayoutManagerForPosition(context, position)) } override fun getItemViewType(position: Int): Int { return when (position) { - POSITION_NORMAL_TABS -> LAYOUT_ID_NORMAL_TAB - POSITION_PRIVATE_TABS -> LAYOUT_ID_PRIVATE_TAB + POSITION_NORMAL_TABS -> NormalBrowserTabViewHolder.LAYOUT_ID + POSITION_PRIVATE_TABS -> PrivateBrowserTabViewHolder.LAYOUT_ID + POSITION_SYNCED_TABS -> SyncedTabViewHolder.LAYOUT_ID else -> throw IllegalStateException("Unknown position.") } } @@ -51,9 +88,10 @@ class TrayPagerAdapter( override fun getItemCount(): Int = TRAY_TABS_COUNT companion object { - const val TRAY_TABS_COUNT = 2 + const val TRAY_TABS_COUNT = 3 - const val POSITION_NORMAL_TABS = 0 - const val POSITION_PRIVATE_TABS = 1 + val POSITION_NORMAL_TABS = Page.NormalTabs.ordinal + val POSITION_PRIVATE_TABS = Page.PrivateTabs.ordinal + val POSITION_SYNCED_TABS = Page.SyncedTabs.ordinal } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TraySheetBehaviorCallback.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TraySheetBehaviorCallback.kt new file mode 100644 index 000000000..5c92df303 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/TraySheetBehaviorCallback.kt @@ -0,0 +1,44 @@ +/* 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.tabstray + +import android.view.View +import androidx.constraintlayout.widget.ConstraintLayout +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_HIDDEN + +class TraySheetBehaviorCallback( + private val behavior: BottomSheetBehavior, + private val trayInteractor: NavigationInteractor +) : BottomSheetBehavior.BottomSheetCallback() { + + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == STATE_HIDDEN) { + trayInteractor.onTabTrayDismissed() + } else if (newState == BottomSheetBehavior.STATE_HALF_EXPANDED) { + // We only support expanded and collapsed states. + // But why?? + behavior.state = STATE_HIDDEN + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) = Unit +} + +fun BottomSheetBehavior.setUpTrayBehavior( + isLandscape: Boolean, + maxNumberOfTabs: Int, + numberForExpandingTray: Int, + navigationInteractor: DefaultNavigationInteractor +) { + addBottomSheetCallback( + TraySheetBehaviorCallback(this, navigationInteractor) + ) + state = if (isLandscape || maxNumberOfTabs >= numberForExpandingTray) { + BottomSheetBehavior.STATE_EXPANDED + } else { + BottomSheetBehavior.STATE_COLLAPSED + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt b/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt deleted file mode 100644 index e94226c8b..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabstray/TrayViewHolders.kt +++ /dev/null @@ -1,41 +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.tabstray - -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.android.extensions.LayoutContainer -import org.mozilla.fenix.R -import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList - -sealed class TrayViewHolder constructor( - override val containerView: View -) : RecyclerView.ViewHolder(containerView), LayoutContainer { - - abstract fun bind(adapter: RecyclerView.Adapter) -} - -class BrowserTabViewHolder( - containerView: View, - interactor: TabsTrayInteractor -) : TrayViewHolder(containerView) { - - private val trayList: BaseBrowserTrayList = itemView.findViewById(R.id.tray_list_item) - - init { - trayList.interactor = interactor - } - - override fun bind(adapter: RecyclerView.Adapter) { - trayList.layoutManager = LinearLayoutManager(itemView.context) - trayList.adapter = adapter - } - - companion object { - const val LAYOUT_ID_NORMAL_TAB = R.layout.normal_browser_tray_list - const val LAYOUT_ID_PRIVATE_TAB = R.layout.private_browser_tray_list - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt index caaa51108..81029500f 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BaseBrowserTrayList.kt @@ -6,18 +6,14 @@ package org.mozilla.fenix.tabstray.browser import android.content.Context import android.util.AttributeSet +import androidx.annotation.VisibleForTesting import androidx.recyclerview.widget.RecyclerView -import mozilla.components.browser.tabstray.TabsAdapter -import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.tabstray.TabsFeature -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper -import org.mozilla.fenix.components.metrics.Event -import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.ext.components import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore import org.mozilla.fenix.tabstray.TrayItem import org.mozilla.fenix.tabstray.ext.filterFromConfig -import org.mozilla.fenix.utils.view.LifecycleViewProvider abstract class BaseBrowserTrayList @JvmOverloads constructor( context: Context, @@ -25,71 +21,78 @@ abstract class BaseBrowserTrayList @JvmOverloads constructor( defStyleAttr: Int = 0 ) : RecyclerView(context, attrs, defStyleAttr), TrayItem { + /** + * The browser tab types we would want to show. + */ enum class BrowserTabType { NORMAL, PRIVATE } + + /** + * A configuration for classes that extend [BaseBrowserTrayList]. + */ data class Configuration(val browserTabType: BrowserTabType) abstract val configuration: Configuration - var interactor: TabsTrayInteractor? = null + lateinit var interactor: TabsTrayInteractor + lateinit var tabsTrayStore: TabsTrayStore + + private val tabsFeature by lazy { + // NB: The use cases here are duplicated because there isn't a nicer + // way to share them without a better dependency injection solution. + val selectTabUseCase = SelectTabUseCaseWrapper( + context.components.analytics.metrics, + context.components.useCases.tabsUseCases.selectTab + ) { + interactor.navigateToBrowser() + } - private val lifecycleProvider = LifecycleViewProvider(this) + val removeTabUseCase = RemoveTabUseCaseWrapper( + context.components.analytics.metrics + ) { sessionId -> + interactor.onDeleteTab(sessionId) + } - private val selectTabUseCase = SelectTabUseCaseWrapper( - context.components.analytics.metrics, - context.components.useCases.tabsUseCases.selectTab - ) { - interactor?.navigateToBrowser() + TabsFeature( + adapter as TabsAdapter, + context.components.core.store, + selectTabUseCase, + removeTabUseCase, + { it.filterFromConfig(configuration) }, + { } + ) } - private val removeTabUseCase = RemoveTabUseCaseWrapper( - context.components.analytics.metrics - ) { sessionId -> - interactor?.tabRemoved(sessionId) + private val swipeToDelete by lazy { + SwipeToDeleteBinding(tabsTrayStore) } - private val tabsFeature by lazy { - ViewBoundFeatureWrapper( - feature = TabsFeature( - adapter as TabsAdapter, - context.components.core.store, - selectTabUseCase, - removeTabUseCase, - { it.filterFromConfig(configuration) }, - { } - ), - owner = lifecycleProvider, - view = this + private val touchHelper by lazy { + TabsTouchHelper( + observable = adapter as TabsAdapter, + onViewHolderTouched = { swipeToDelete.isSwipeable }, + onViewHolderDraw = { context.components.settings.listTabView } ) } override fun onAttachedToWindow() { super.onAttachedToWindow() - // This is weird, but I don't have a better solution right now: We need to keep a - // lazy reference to the feature/adapter so that we do not re-create - // it every time it's attached. This reference is our way to init. - tabsFeature - } -} + tabsFeature.start() + swipeToDelete.start() -internal class SelectTabUseCaseWrapper( - private val metrics: MetricController, - private val selectTab: TabsUseCases.SelectTabUseCase, - private val onSelect: (String) -> Unit -) : TabsUseCases.SelectTabUseCase { - override fun invoke(tabId: String) { - metrics.track(Event.OpenedExistingTab) - selectTab(tabId) - onSelect(tabId) + touchHelper.attachToRecyclerView(this) } -} -internal class RemoveTabUseCaseWrapper( - private val metrics: MetricController, - private val onRemove: (String) -> Unit -) : TabsUseCases.RemoveTabUseCase { - override fun invoke(sessionId: String) { - metrics.track(Event.ClosedExistingTab) - onRemove(sessionId) + @VisibleForTesting + public override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + + tabsFeature.stop() + swipeToDelete.stop() + + // Notify the adapter that it is released from the view preemptively. + adapter?.onDetachedFromRecyclerView(this) + + touchHelper.attachToRecyclerView(null) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt new file mode 100644 index 000000000..3eb5550f3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTabsAdapter.kt @@ -0,0 +1,118 @@ +/* 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.tabstray.browser + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.tab_tray_item.view.* +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.browser.thumbnails.loader.ThumbnailLoader +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TabsTrayViewHolder + +/** + * A [RecyclerView.Adapter] for browser tabs. + */ +class BrowserTabsAdapter( + private val context: Context, + private val interactor: BrowserTrayInteractor, + private val store: TabsTrayStore, + delegate: Observable = ObserverRegistry() +) : TabsAdapter(delegate) { + + /** + * The layout types for the tabs. + */ + enum class ViewType(val layoutRes: Int) { + LIST(R.layout.tab_tray_item), + GRID(R.layout.tab_tray_grid_item) + } + + /** + * Tracks the selected tabs in multi-select mode. + */ + var selectionHolder: SelectionHolder? = null + + private val selectedItemAdapterBinding = SelectedItemAdapterBinding(store, this) + private val imageLoader = ThumbnailLoader(context.components.core.thumbnailStorage) + + override fun getItemViewType(position: Int): Int { + return if (context.components.settings.gridTabView) { + ViewType.GRID.layoutRes + } else { + ViewType.LIST.layoutRes + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TabsTrayViewHolder { + val view = LayoutInflater.from(parent.context).inflate(viewType, parent, false) + + return when (viewType) { + ViewType.GRID.layoutRes -> + TabsTrayGridViewHolder(imageLoader, interactor, store, selectionHolder, view) + else -> + TabsTrayListViewHolder(imageLoader, interactor, store, selectionHolder, view) + } + } + + override fun onBindViewHolder(holder: TabsTrayViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + + holder.tab?.let { tab -> + holder.itemView.mozac_browser_tabstray_close.setOnClickListener { + interactor.close(tab) + } + + selectionHolder?.let { + holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(tab)) + } + } + } + + /** + * Over-ridden [onBindViewHolder] that uses the payloads to notify the selected tab how to + * display itself. + */ + override fun onBindViewHolder(holder: TabsTrayViewHolder, position: Int, payloads: List) { + val tabs = tabs ?: return + + if (tabs.list.isEmpty()) return + + if (payloads.isEmpty()) { + onBindViewHolder(holder, position) + return + } + + if (position == tabs.selectedIndex) { + if (payloads.contains(PAYLOAD_HIGHLIGHT_SELECTED_ITEM)) { + holder.updateSelectedTabIndicator(true) + } else if (payloads.contains(PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM)) { + holder.updateSelectedTabIndicator(false) + } + } + + selectionHolder?.let { + holder.showTabIsMultiSelectEnabled(it.selectedItems.contains(holder.tab)) + } + } + + override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + selectedItemAdapterBinding.start() + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + selectedItemAdapterBinding.stop() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt new file mode 100644 index 000000000..49cb4784d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/BrowserTrayInteractor.kt @@ -0,0 +1,138 @@ +/* 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.tabstray.browser + +import android.content.Context +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.tabstray.Tab +import mozilla.components.feature.tabs.TabsUseCases +import mozilla.components.support.base.feature.UserInteractionHandler +import org.mozilla.fenix.selection.SelectionInteractor +import org.mozilla.fenix.tabstray.TabsTrayAction +import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.tabstray.TabsTrayController +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TrayPagerAdapter +import org.mozilla.fenix.tabstray.ext.numberOfGridColumns +import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.tabstray.TabsTrayState.Mode +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * For interacting with UI that is specifically for [BaseBrowserTrayList] and other browser + * tab tray views. + */ +interface BrowserTrayInteractor : SelectionInteractor, UserInteractionHandler { + + /** + * Close the tab. + */ + fun close(tab: Tab) + + /** + * Returns the appropriate [RecyclerView.LayoutManager] to be used at [position]. + */ + fun getLayoutManagerForPosition(context: Context, position: Int): RecyclerView.LayoutManager + + /** + * TabTray's Floating Action Button clicked. + */ + fun onFabClicked(isPrivate: Boolean) +} + +/** + * A default implementation of [BrowserTrayInteractor]. + */ +class DefaultBrowserTrayInteractor( + private val store: TabsTrayStore, + private val trayInteractor: TabsTrayInteractor, + private val controller: TabsTrayController, + private val selectTab: TabsUseCases.SelectTabUseCase, + private val settings: Settings, + private val metrics: MetricController +) : BrowserTrayInteractor { + + private val selectTabWrapper by lazy { + SelectTabUseCaseWrapper(metrics, selectTab) { + trayInteractor.navigateToBrowser() + } + } + + private val removeTabWrapper by lazy { + RemoveTabUseCaseWrapper(metrics) { + // Handle removal from the interactor where we can also handle "undo" visuals. + trayInteractor.onDeleteTab(it) + } + } + + /** + * See [SelectionInteractor.open] + */ + override fun open(item: Tab) { + selectTabWrapper.invoke(item.id) + trayInteractor.navigateToBrowser() + } + + /** + * See [BrowserTrayInteractor.close]. + */ + override fun close(tab: Tab) { + removeTabWrapper.invoke(tab.id) + } + + /** + * See [SelectionInteractor.select] + */ + override fun select(item: Tab) { + store.dispatch(TabsTrayAction.AddSelectTab(item)) + } + + /** + * See [SelectionInteractor.deselect] + */ + override fun deselect(item: Tab) { + store.dispatch(TabsTrayAction.RemoveSelectTab(item)) + } + + /** + * See [UserInteractionHandler.onBackPressed] + * + * TODO move this to the navigation interactor when it lands. + */ + override fun onBackPressed(): Boolean { + if (store.state.mode is Mode.Select) { + store.dispatch(TabsTrayAction.ExitSelectMode) + return true + } + return false + } + + override fun getLayoutManagerForPosition( + context: Context, + position: Int + ): RecyclerView.LayoutManager { + if (position == TrayPagerAdapter.POSITION_SYNCED_TABS) { + // Lists are just Grids with one column :) + return GridLayoutManager(context, 1) + } + + // Normal/Private tabs + val numberOfColumns = if (settings.gridTabView) { + context.numberOfGridColumns + } else { + 1 + } + + return GridLayoutManager(context, numberOfColumns) + } + + /** + * See [BrowserTrayInteractor.onFabClicked] + */ + override fun onFabClicked(isPrivate: Boolean) { + controller.onNewTabTapped(isPrivate) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt new file mode 100644 index 000000000..a725364dc --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectedItemAdapterBinding.kt @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabstray.browser + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.browser.tabstray.TabsAdapter.Companion.PAYLOAD_HIGHLIGHT_SELECTED_ITEM +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.tabstray.TabsTrayState.Mode +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * Notifies the adapter when the selection mode changes. + */ +class SelectedItemAdapterBinding( + val store: TabsTrayStore, + val adapter: BrowserTabsAdapter +) : LifecycleAwareFeature { + private var scope: CoroutineScope? = null + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + scope = store.flowScoped { flow -> + flow.map { it.mode } + // ignore initial mode update; the adapter is already in an updated state. + .drop(1) + .ifChanged() + .collect { mode -> + notifyAdapter(mode) + } + } + } + + override fun stop() { + scope?.cancel() + } + + private fun notifyAdapter(mode: Mode) = with(adapter) { + if (mode == Mode.Normal) { + notifyItemRangeChanged(0, itemCount, PAYLOAD_HIGHLIGHT_SELECTED_ITEM) + } else { + notifyItemRangeChanged(0, itemCount, PAYLOAD_DONT_HIGHLIGHT_SELECTED_ITEM) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt new file mode 100644 index 000000000..8850049a4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionBannerBinding.kt @@ -0,0 +1,141 @@ +/* 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.tabstray.browser + +import android.content.Context +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import kotlinx.android.synthetic.main.component_tabstray2.view.exit_multi_select +import kotlinx.android.synthetic.main.component_tabstray2.view.multiselect_title +import kotlinx.android.synthetic.main.tabstray_multiselect_items.view.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.R +import org.mozilla.fenix.components.AbstractBinding +import org.mozilla.fenix.tabstray.NavigationInteractor +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TabsTrayAction.ExitSelectMode +import org.mozilla.fenix.tabstray.TabsTrayState.Mode +import org.mozilla.fenix.tabstray.TabsTrayState.Mode.Select +import org.mozilla.fenix.tabstray.ext.showWithTheme + +/** + * A binding that shows/hides the multi-select banner of the selected count of tabs. + * + * @property context An Android context. + * @property store The TabsTrayStore instance. + * @property navInteractor An instance of [NavigationInteractor] for navigating on menu clicks. + * @property tabsTrayInteractor An instance of [TabsTrayInteractor] for handling deletion. + * @property containerView The view in the layout that contains all the implicit multi-select + * views. NB: This parameter is a bit opaque and requires a larger layout refactor to correct. + * @property backgroundView The background view that we want to alter when changing [Mode]. + * @property showOnSelectViews A variable list of views that will be made visible when in select mode. + * @property showOnNormalViews A variable list of views that will be made visible when in normal mode. + */ +@Suppress("LongParameterList") +class SelectionBannerBinding( + private val context: Context, + private val store: TabsTrayStore, + private val navInteractor: NavigationInteractor, + private val tabsTrayInteractor: TabsTrayInteractor, + private val containerView: View, + private val backgroundView: View, + private val showOnSelectViews: VisibilityModifier, + private val showOnNormalViews: VisibilityModifier +) : AbstractBinding(store) { + + /** + * A holder of views that will be used by having their [View.setVisibility] modified. + */ + class VisibilityModifier(vararg val views: View) + + private var isPreviousModeSelect = false + + override fun start() { + super.start() + + initListeners(containerView) + } + + override suspend fun onState(flow: Flow) { + flow.map { it.mode } + // ignore initial mode update; we never start in select mode. + .drop(1) + .ifChanged() + .collect { mode -> + val isSelectMode = mode is Select + + showOnSelectViews.views.forEach { + it.isVisible = isSelectMode + } + + showOnNormalViews.views.forEach { + it.isVisible = isSelectMode.not() + } + + updateBackgroundColor(isSelectMode) + + updateSelectTitle(isSelectMode, mode.selectedTabs.size) + + isPreviousModeSelect = isSelectMode + } + } + + private fun initListeners(containerView: View) { + containerView.share_multi_select.setOnClickListener { + navInteractor.onShareTabs(store.state.mode.selectedTabs) + } + + containerView.collect_multi_select.setOnClickListener { + navInteractor.onSaveToCollections(store.state.mode.selectedTabs) + } + + containerView.exit_multi_select.setOnClickListener { + store.dispatch(ExitSelectMode) + } + + containerView.menu_multi_select.setOnClickListener { anchor -> + val menu = SelectionMenuIntegration( + context, + store, + navInteractor, + tabsTrayInteractor + ).build() + + menu.showWithTheme(anchor) + } + } + + @VisibleForTesting + private fun updateBackgroundColor(isSelectMode: Boolean) { + // memoize to avoid setting the background unnecessarily. + if (isPreviousModeSelect != isSelectMode) { + val colorResource = if (isSelectMode) { + R.color.accent_normal_theme + } else { + R.color.foundation_normal_theme + } + + val color = ContextCompat.getColor(backgroundView.context, colorResource) + + backgroundView.setBackgroundColor(color) + } + } + + @VisibleForTesting + private fun updateSelectTitle(selectedMode: Boolean, tabCount: Int) { + if (selectedMode) { + containerView.multiselect_title.text = + context.getString(R.string.tab_tray_multi_select_title, tabCount) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt new file mode 100644 index 000000000..6560b8d93 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionHandleBinding.kt @@ -0,0 +1,108 @@ +/* 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.tabstray.browser + +import android.view.View +import android.view.ViewGroup +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.content.ContextCompat +import androidx.core.view.updateLayoutParams +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.map +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.R +import org.mozilla.fenix.components.AbstractBinding +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayState.Mode +import org.mozilla.fenix.tabstray.TabsTrayStore + +private const val NORMAL_HANDLE_PERCENT_WIDTH = 0.1F + +/** + * Various layout updates that need to be applied to the "handle" view when switching + * between [Mode]. + * + * @param store The TabsTrayStore instance. + * @property handle The "handle" of the Tabs Tray that is used to drag the tray open/close. + * @property containerLayout The [ConstraintLayout] that contains the "handle". + */ +class SelectionHandleBinding( + store: TabsTrayStore, + private val handle: View, + private val containerLayout: ConstraintLayout +) : AbstractBinding(store) { + + private var isPreviousModeSelect = false + + override suspend fun onState(flow: Flow) { + flow.map { it.mode } + // ignore initial mode update; we never start in select mode. + .drop(1) + .ifChanged() + .collect { mode -> + val isSelectMode = mode is Mode.Select + + // memoize to avoid unnecessary layout updates. + if (isPreviousModeSelect != isSelectMode) { + updateLayoutParams(handle, isSelectMode) + + updateBackgroundColor(handle, isSelectMode) + + updateWidthPercent(containerLayout, handle, isSelectMode) + } + + isPreviousModeSelect = isSelectMode + } + } + + private fun updateLayoutParams(handle: View, multiselect: Boolean) { + handle.updateLayoutParams { + height = handle.resources.getDimensionPixelSize( + if (multiselect) { + R.dimen.tab_tray_multiselect_handle_height + } else { + R.dimen.bottom_sheet_handle_height + } + ) + topMargin = handle.resources.getDimensionPixelSize( + if (multiselect) { + R.dimen.tab_tray_multiselect_handle_top_margin + } else { + R.dimen.bottom_sheet_handle_top_margin + } + ) + } + } + + private fun updateBackgroundColor(handle: View, multiselect: Boolean) { + val colorResource = if (multiselect) { + R.color.accent_normal_theme + } else { + R.color.secondary_text_normal_theme + } + + val color = ContextCompat.getColor(handle.context, colorResource) + + handle.setBackgroundColor(color) + } + + private fun updateWidthPercent( + container: ConstraintLayout, + handle: View, + multiselect: Boolean + ) { + val widthPercent = if (multiselect) 1F else NORMAL_HANDLE_PERCENT_WIDTH + container.run { + ConstraintSet().apply { + clone(this@run) + constrainPercentWidth(handle.id, widthPercent) + applyTo(this@run) + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.kt new file mode 100644 index 000000000..1a789acb6 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenu.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.tabstray.browser + +import android.content.Context +import mozilla.components.browser.menu.BrowserMenuBuilder +import mozilla.components.browser.menu.item.SimpleBrowserMenuItem +import org.mozilla.fenix.R + +class SelectionMenu( + private val context: Context, + private val onItemTapped: (Item) -> Unit = {} +) { + sealed class Item { + object BookmarkTabs : Item() + object DeleteTabs : Item() + } + + val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } + + private val menuItems by lazy { + listOf( + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_multiselect_menu_item_bookmark), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.BookmarkTabs) + }, + + SimpleBrowserMenuItem( + context.getString(R.string.tab_tray_multiselect_menu_item_close), + textColorResource = R.color.primary_text_normal_theme + ) { + onItemTapped.invoke(Item.DeleteTabs) + } + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt new file mode 100644 index 000000000..a865ebae7 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SelectionMenuIntegration.kt @@ -0,0 +1,41 @@ +/* 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.tabstray.browser + +import android.content.Context +import androidx.annotation.VisibleForTesting +import mozilla.components.browser.menu.BrowserMenuBuilder +import org.mozilla.fenix.tabstray.NavigationInteractor +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.utils.Do + +class SelectionMenuIntegration( + private val context: Context, + private val store: TabsTrayStore, + private val navInteractor: NavigationInteractor, + private val trayInteractor: TabsTrayInteractor +) { + private val menu by lazy { + SelectionMenu(context, ::handleMenuClicked) + } + + /** + * Builds the internal menu items list. See [BrowserMenuBuilder.build]. + */ + fun build() = menu.menuBuilder.build(context) + + @VisibleForTesting + internal fun handleMenuClicked(item: SelectionMenu.Item) { + Do exhaustive when (item) { + is SelectionMenu.Item.BookmarkTabs -> navInteractor.onSaveToBookmarks( + store.state.mode.selectedTabs + ) + is SelectionMenu.Item.DeleteTabs -> trayInteractor.onDeleteTabs( + store.state.mode.selectedTabs + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt new file mode 100644 index 000000000..fd7052e81 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/SwipeToDeleteBinding.kt @@ -0,0 +1,42 @@ +/* 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.tabstray.browser + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.lib.state.ext.flowScoped +import mozilla.components.support.base.feature.LifecycleAwareFeature +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * Notifies whether a tab is accessible for using the swipe-to-delete gesture. + */ +class SwipeToDeleteBinding( + private val store: TabsTrayStore +) : LifecycleAwareFeature { + private var scope: CoroutineScope? = null + var isSwipeable = false + private set + + @OptIn(ExperimentalCoroutinesApi::class) + override fun start() { + scope = store.flowScoped { flow -> + flow.map { it.mode } + .ifChanged() + .collect { mode -> + isSwipeable = mode == TabsTrayState.Mode.Normal + } + } + } + + override fun stop() { + scope?.cancel() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt new file mode 100644 index 000000000..8e4820cd1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsAdapter.kt @@ -0,0 +1,64 @@ +/* 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.tabstray.browser + +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.tabstray.TabViewHolder +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.concept.tabstray.Tabs +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry + +/** + * RecyclerView adapter implementation to display a list/grid of tabs. + * + * The previous tabs adapter was very restrictive and required Fenix to jump through + * may hoops to access and update certain methods. An abstract adapter is easier to manage + * for Android UI APIs. + * + * TODO Let's upstream this to AC with tests. + * + * @param delegate TabsTray.Observer registry to allow `TabsAdapter` to conform to `Observable`. + */ +abstract class TabsAdapter( + delegate: Observable = ObserverRegistry() +) : RecyclerView.Adapter(), TabsTray, Observable by delegate { + + protected var tabs: Tabs? = null + protected var styling: TabsTrayStyling = TabsTrayStyling() + + @CallSuper + override fun updateTabs(tabs: Tabs) { + this.tabs = tabs + + notifyObservers { onTabsUpdated() } + } + + @CallSuper + override fun onBindViewHolder(holder: T, position: Int) { + val tabs = tabs ?: return + + holder.bind(tabs.list[position], isTabSelected(tabs, position), styling, this) + } + + override fun getItemCount(): Int = tabs?.list?.size ?: 0 + + final override fun isTabSelected(tabs: Tabs, position: Int): Boolean = + tabs.selectedIndex == position + + final override fun onTabsChanged(position: Int, count: Int) = + notifyItemRangeChanged(position, count) + + final override fun onTabsInserted(position: Int, count: Int) = + notifyItemRangeInserted(position, count) + + final override fun onTabsMoved(fromPosition: Int, toPosition: Int) = + notifyItemMoved(fromPosition, toPosition) + + final override fun onTabsRemoved(position: Int, count: Int) = + notifyItemRangeRemoved(position, count) +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt new file mode 100644 index 000000000..25fc74c19 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTouchHelper.kt @@ -0,0 +1,139 @@ +/* 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.tabstray.browser + +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import androidx.appcompat.content.res.AppCompatResources +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.browser.tabstray.TabTouchCallback +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.ktx.android.content.getColorFromAttr +import mozilla.components.support.ktx.android.content.getDrawableWithTint +import mozilla.components.support.ktx.android.util.dpToPx +import org.mozilla.fenix.R +import org.mozilla.fenix.home.sessioncontrol.SwipeToDeleteCallback + +/** + * A callback for consumers to know when a [RecyclerView.ViewHolder] is about to be touched. + * Return false if the custom behaviour should be ignored. + */ +typealias OnViewHolderTouched = (RecyclerView.ViewHolder) -> Boolean + +/** + * A callback for consumers to know when a [RecyclerView.ViewHolder] is about to be drawn. + * Return false if the custom drawing should be ignored. + */ +typealias OnViewHolderToDraw = (RecyclerView.ViewHolder) -> Boolean + +/** + * An [ItemTouchHelper] for handling tab swiping to delete. + * + * @param onViewHolderTouched See [OnViewHolderTouched]. + */ +class TabsTouchHelper( + observable: Observable, + onViewHolderTouched: OnViewHolderTouched = { true }, + onViewHolderDraw: OnViewHolderToDraw = { true }, + delegate: Callback = TouchCallback(observable, onViewHolderTouched, onViewHolderDraw) +) : ItemTouchHelper(delegate) + +/** + * An [ItemTouchHelper.Callback] for drawing custom layouts on [RecyclerView.ViewHolder] interactions. + * + * @param onViewHolderTouched invoked when a tab is about to be swiped. See [OnViewHolderTouched]. + */ +class TouchCallback( + observable: Observable, + private val onViewHolderTouched: OnViewHolderTouched, + private val onViewHolderDraw: OnViewHolderToDraw +) : TabTouchCallback(observable) { + + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + if (!onViewHolderTouched.invoke(viewHolder)) { + return ItemTouchHelper.Callback.makeFlag(ACTION_STATE_IDLE, 0) + } + + return super.getMovementFlags(recyclerView, viewHolder) + } + + override fun onChildDraw( + c: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean + ) { + super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) + + if (!onViewHolderDraw.invoke(viewHolder)) { + return + } + + val icon = recyclerView.context.getDrawableWithTint( + R.drawable.ic_delete, + recyclerView.context.getColorFromAttr(R.attr.destructive) + )!! + val background = AppCompatResources.getDrawable( + recyclerView.context, + R.drawable.swipe_delete_background + )!! + val itemView = viewHolder.itemView + val iconLeft: Int + val iconRight: Int + val margin = + SwipeToDeleteCallback.MARGIN.dpToPx(recyclerView.resources.displayMetrics) + val iconWidth = icon.intrinsicWidth + val iconHeight = icon.intrinsicHeight + val cellHeight = itemView.bottom - itemView.top + val iconTop = itemView.top + (cellHeight - iconHeight) / 2 + val iconBottom = iconTop + iconHeight + + when { + dX > 0 -> { // Swiping to the right + iconLeft = itemView.left + margin + iconRight = itemView.left + margin + iconWidth + background.setBounds( + itemView.left, itemView.top, + (itemView.left + dX).toInt() + SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET, + itemView.bottom + ) + icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) + draw(background, icon, c) + } + dX < 0 -> { // Swiping to the left + iconLeft = itemView.right - margin - iconWidth + iconRight = itemView.right - margin + background.setBounds( + (itemView.right + dX).toInt() - SwipeToDeleteCallback.BACKGROUND_CORNER_OFFSET, + itemView.top, itemView.right, itemView.bottom + ) + icon.setBounds(iconLeft, iconTop, iconRight, iconBottom) + draw(background, icon, c) + } + else -> { // View not swiped + background.setBounds(0, 0, 0, 0) + icon.setBounds(0, 0, 0, 0) + } + } + } + + private fun draw( + background: Drawable, + icon: Drawable, + c: Canvas + ) { + background.draw(c) + icon.draw(c) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt new file mode 100644 index 000000000..51915b03d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayGridViewHolder.kt @@ -0,0 +1,61 @@ +/* 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.tabstray.browser + +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.AppCompatImageButton +import mozilla.components.browser.tabstray.TabsTrayStyling +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.concept.tabstray.Tab +import mozilla.components.concept.tabstray.TabsTray +import mozilla.components.support.base.observer.Observable +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.increaseTapArea +import kotlin.math.max +import kotlinx.android.synthetic.main.tab_tray_grid_item.view.tab_tray_grid_item +import org.mozilla.fenix.tabstray.TabsTrayViewHolder +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * A RecyclerView ViewHolder implementation for "tab" items with grid layout. + */ +class TabsTrayGridViewHolder( + imageLoader: ImageLoader, + override val browserTrayInteractor: BrowserTrayInteractor, + store: TabsTrayStore, + selectionHolder: SelectionHolder? = null, + itemView: View +) : TabsTrayViewHolder(itemView, imageLoader, store, selectionHolder) { + + private val closeButton: AppCompatImageButton = itemView.findViewById(R.id.mozac_browser_tabstray_close) + + override val thumbnailSize: Int + get() = max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_grid_item_thumbnail_width) + ) + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + itemView.tab_tray_grid_item.background = if (showAsSelected) { + AppCompatResources.getDrawable(itemView.context, R.drawable.tab_tray_grid_item_selected_border) + } else { + null + } + return + } + + override fun bind( + tab: Tab, + isSelected: Boolean, + styling: TabsTrayStyling, + observable: Observable + ) { + super.bind(tab, isSelected, styling, observable) + + closeButton.increaseTapArea(GRID_ITEM_CLOSE_BUTTON_EXTRA_DPS) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt new file mode 100644 index 000000000..cc65f6846 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/TabsTrayListViewHolder.kt @@ -0,0 +1,46 @@ +/* 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.tabstray.browser + +import android.view.View +import androidx.core.content.ContextCompat +import mozilla.components.concept.base.images.ImageLoader +import mozilla.components.concept.tabstray.Tab +import org.mozilla.fenix.R +import org.mozilla.fenix.tabstray.TabsTrayViewHolder +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayStore +import kotlin.math.max + +/** + * A RecyclerView ViewHolder implementation for "tab" items with list layout. + */ +class TabsTrayListViewHolder( + imageLoader: ImageLoader, + override val browserTrayInteractor: BrowserTrayInteractor, + store: TabsTrayStore, + selectionHolder: SelectionHolder? = null, + itemView: View +) : TabsTrayViewHolder(itemView, imageLoader, store, selectionHolder) { + override val thumbnailSize: Int + get() = max( + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_height), + itemView.resources.getDimensionPixelSize(R.dimen.tab_tray_list_item_thumbnail_width) + ) + + override fun updateSelectedTabIndicator(showAsSelected: Boolean) { + val color = if (showAsSelected) { + R.color.tab_tray_item_selected_background_normal_theme + } else { + R.color.tab_tray_item_background_normal_theme + } + itemView.setBackgroundColor( + ContextCompat.getColor( + itemView.context, + color + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/browser/UseCases.kt b/app/src/main/java/org/mozilla/fenix/tabstray/browser/UseCases.kt new file mode 100644 index 000000000..7dbfaa457 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/browser/UseCases.kt @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabstray.browser + +import mozilla.components.feature.tabs.TabsUseCases +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.MetricController + +class SelectTabUseCaseWrapper( + private val metrics: MetricController, + private val selectTab: TabsUseCases.SelectTabUseCase, + private val onSelect: (String) -> Unit +) : TabsUseCases.SelectTabUseCase { + override fun invoke(tabId: String) { + metrics.track(Event.OpenedExistingTab) + selectTab(tabId) + onSelect(tabId) + } +} + +class RemoveTabUseCaseWrapper( + private val metrics: MetricController, + private val onRemove: (String) -> Unit +) : TabsUseCases.RemoveTabUseCase { + override fun invoke(sessionId: String) { + metrics.track(Event.ClosedExistingTab) + onRemove(sessionId) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserMenu.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserMenu.kt new file mode 100644 index 000000000..ec98a59ec --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserMenu.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.tabstray.ext + +import android.view.View +import androidx.cardview.widget.CardView +import androidx.core.content.ContextCompat +import mozilla.components.browser.menu.BrowserMenu +import org.mozilla.fenix.R + +/** + * Invokes [BrowserMenu.show] and applies the default theme color background. + */ +fun BrowserMenu.showWithTheme(view: View) { + show(view).also { popupMenu -> + (popupMenu.contentView as? CardView)?.setCardBackgroundColor( + ContextCompat.getColor( + view.context, + R.color.foundation_normal_theme + ) + ) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserStore.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserStore.kt new file mode 100644 index 000000000..c5577a13e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/BrowserStore.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.tabstray.ext + +import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.concept.tabstray.Tab + +/** + * Find and extract a list [TabSessionState] from the [BrowserStore] using the IDs from [tabs]. + */ +fun BrowserStore.getTabSessionState(tabs: Collection): List { + return tabs.mapNotNull { + state.findTab(it.id) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.kt new file mode 100644 index 000000000..82f522ec6 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/Context.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.tabstray.ext + +import android.content.Context + +private const val MIN_COLUMN_WIDTH_DP = 180 + +/** + * Returns the number of grid columns we can fit on the screen in the tabs tray. + */ +internal val Context.numberOfGridColumns: Int + get() { + val displayMetrics = resources.displayMetrics + val screenWidthDp = displayMetrics.widthPixels / displayMetrics.density + return (screenWidthDp / MIN_COLUMN_WIDTH_DP).toInt() + } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabLayout.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabLayout.kt new file mode 100644 index 000000000..dfba62b0d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabLayout.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.tabstray.ext + +import com.google.android.material.tabs.TabLayout +import org.mozilla.fenix.tabstray.TrayPagerAdapter + +fun TabLayout.isNormalModeSelected(): Boolean { + return selectedTabPosition == TrayPagerAdapter.POSITION_NORMAL_TABS +} + +fun TabLayout.isPrivateModeSelected(): Boolean { + return selectedTabPosition == TrayPagerAdapter.POSITION_PRIVATE_TABS +} + +fun TabLayout.isSyncedModeSelected(): Boolean { + return selectedTabPosition == TrayPagerAdapter.POSITION_SYNCED_TABS +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt index cf11c8c39..777e4f47e 100644 --- a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabSessionState.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.tabstray.ext import mozilla.components.browser.state.state.TabSessionState +import org.mozilla.fenix.tabstray.Page import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.BrowserTabType.PRIVATE import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList.Configuration @@ -13,3 +14,9 @@ fun TabSessionState.filterFromConfig(configuration: Configuration): Boolean { return content.private == isPrivate } + +fun TabSessionState.getTrayPosition(): Int = + when (content.private) { + true -> Page.NormalTabs.ordinal + false -> Page.NormalTabs.ordinal + } diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabsTrayState.kt b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabsTrayState.kt new file mode 100644 index 000000000..96434cc5d --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/ext/TabsTrayState.kt @@ -0,0 +1,12 @@ +/* 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.tabstray.ext + +import org.mozilla.fenix.tabstray.TabsTrayState.Mode + +/** + * A helper to check if we're in [Mode.Select] mode. + */ +fun Mode.isSelect() = this is Mode.Select diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt new file mode 100644 index 000000000..e07c7008f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncButtonBinding.kt @@ -0,0 +1,35 @@ +/* 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.tabstray.syncedtabs + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged +import org.mozilla.fenix.components.AbstractBinding +import org.mozilla.fenix.tabstray.TabsTrayState +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * An [AbstractBinding] that invokes the [onSyncNow] callback when the [TabsTrayState.syncing] is + * set. + * + * This binding is useful for connecting with [SyncedTabsView.Listener]. + */ +class SyncButtonBinding( + tabsTrayStore: TabsTrayStore, + private val onSyncNow: () -> Unit +) : AbstractBinding(tabsTrayStore) { + override suspend fun onState(flow: Flow) { + flow.map { it.syncing } + .ifChanged() + .collect { syncingNow -> + if (syncingNow) { + onSyncNow() + } + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt new file mode 100644 index 000000000..d659f28a9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/SyncedTabsTrayLayout.kt @@ -0,0 +1,117 @@ +/* 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.tabstray.syncedtabs + +import android.content.Context +import android.util.AttributeSet +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.fragment.app.findFragment +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import kotlinx.android.synthetic.main.component_sync_tabs_tray_layout.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import mozilla.components.browser.storage.sync.SyncedDeviceTabs +import mozilla.components.feature.syncedtabs.SyncedTabsFeature +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import mozilla.components.support.base.observer.Observable +import mozilla.components.support.base.observer.ObserverRegistry +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.sync.SyncedTabsAdapter +import org.mozilla.fenix.sync.ext.toAdapterItem +import org.mozilla.fenix.sync.ext.toStringRes +import org.mozilla.fenix.tabstray.TabsTrayAction +import org.mozilla.fenix.tabstray.TabsTrayFragment +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.TrayItem +import org.mozilla.fenix.utils.view.LifecycleViewProvider + +class SyncedTabsTrayLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), SyncedTabsView, TrayItem, + Observable by ObserverRegistry() { + + private val lifecycleProvider = LifecycleViewProvider(this) + private val coroutineScope = CoroutineScope(Dispatchers.Main) + + private val syncedTabsFeature by lazy { + SyncedTabsFeature( + context = context, + storage = context.components.backgroundServices.syncedTabsStorage, + accountManager = context.components.backgroundServices.accountManager, + view = this, + lifecycleOwner = lifecycleProvider, + onTabClicked = { + // We can ignore this callback here because we're not connecting the adapter + // back to the feature. This works fine in other features, but passing the listener + // to other components in this case is annoying. + } + ) + } + + private val syncButtonBinding by lazy { + SyncButtonBinding(tabsTrayStore) { + listener?.onRefresh() + } + } + + lateinit var tabsTrayStore: TabsTrayStore + + override var listener: SyncedTabsView.Listener? = null + + override fun displaySyncedTabs(syncedTabs: List) { + coroutineScope.launch { + (synced_tabs_list.adapter as SyncedTabsAdapter).updateData(syncedTabs) + } + } + + override fun onError(error: SyncedTabsView.ErrorType) { + coroutineScope.launch { + // We may still be displaying a "loading" spinner, hide it. + stopLoading() + + val navController: NavController? = try { + findFragment().findNavController() + } catch (exception: IllegalStateException) { + null + } + + val descriptionResId = error.toStringRes() + val errorItem = error.toAdapterItem(descriptionResId, navController) + + val errorList: List = listOf(errorItem) + (synced_tabs_list.adapter as SyncedTabsAdapter).submitList(errorList) + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + syncedTabsFeature.start() + syncButtonBinding.start() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + + syncedTabsFeature.stop() + syncButtonBinding.stop() + + coroutineScope.cancel() + } + + override fun stopLoading() { + tabsTrayStore.dispatch(TabsTrayAction.SyncCompleted) + } + + /** + * Do nothing; the UI is handled with FloatingActionButtonBinding. + */ + override fun startLoading() = Unit +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/TabClickDelegate.kt b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/TabClickDelegate.kt new file mode 100644 index 000000000..53f93ab6b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/syncedtabs/TabClickDelegate.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.tabstray.syncedtabs + +import mozilla.components.browser.storage.sync.Tab +import mozilla.components.feature.syncedtabs.view.SyncedTabsView +import org.mozilla.fenix.tabstray.NavigationInteractor + +/** + * A wrapper class that handles tab clicks from a Synced Tabs list. + */ +class TabClickDelegate( + private val interactor: NavigationInteractor +) : SyncedTabsView.Listener { + override fun onTabClicked(tab: Tab) { + interactor.onSyncedTabClicked(tab) + } + + override fun onRefresh() = Unit +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractTrayViewHolder.kt new file mode 100644 index 000000000..ebe7725e5 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/AbstractTrayViewHolder.kt @@ -0,0 +1,23 @@ +/* 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.tabstray.viewholders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.extensions.LayoutContainer +import org.mozilla.fenix.tabstray.TrayPagerAdapter + +/** + * An abstract [RecyclerView.ViewHolder] for [TrayPagerAdapter] items. + */ +abstract class AbstractTrayViewHolder constructor( + override val containerView: View +) : RecyclerView.ViewHolder(containerView), LayoutContainer { + + abstract fun bind( + adapter: RecyclerView.Adapter, + layoutManager: RecyclerView.LayoutManager + ) +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/BaseBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/BaseBrowserTabViewHolder.kt new file mode 100644 index 000000000..faecdd921 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/BaseBrowserTabViewHolder.kt @@ -0,0 +1,73 @@ +/* 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.tabstray.viewholders + +import android.view.View +import android.view.View.GONE +import android.view.View.VISIBLE +import android.widget.TextView +import androidx.annotation.CallSuper +import androidx.recyclerview.widget.RecyclerView +import org.mozilla.fenix.R +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.browser.BaseBrowserTrayList + +/** + * A shared view holder for browser tabs tray list. + */ +abstract class BaseBrowserTabViewHolder( + containerView: View, + tabsTrayStore: TabsTrayStore, + interactor: TabsTrayInteractor, + private val currentTabIndex: Int +) : AbstractTrayViewHolder(containerView) { + + private val trayList: BaseBrowserTrayList = itemView.findViewById(R.id.tray_list_item) + private val emptyList: TextView = itemView.findViewById(R.id.tab_tray_empty_view) + abstract val emptyStringText: String + + init { + trayList.interactor = interactor + trayList.tabsTrayStore = tabsTrayStore + emptyList.text = emptyStringText + } + + @CallSuper + override fun bind( + adapter: RecyclerView.Adapter, + layoutManager: RecyclerView.LayoutManager + ) { + adapter.registerAdapterDataObserver(OneTimeAdapterObserver(adapter) { + trayList.scrollToPosition(currentTabIndex) + updateTrayVisibility(adapter.itemCount) + }) + trayList.layoutManager = layoutManager + trayList.adapter = adapter + } + + private fun updateTrayVisibility(size: Int) { + if (size == 0) { + trayList.visibility = GONE + emptyList.visibility = VISIBLE + } else { + trayList.visibility = VISIBLE + emptyList.visibility = GONE + } + } +} + +/** + * Observes the adapter and invokes the callback when data is first inserted. + */ +class OneTimeAdapterObserver( + private val adapter: RecyclerView.Adapter, + private val onAdapterReady: () -> Unit +) : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + onAdapterReady.invoke() + adapter.unregisterAdapterDataObserver(this) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt new file mode 100644 index 000000000..61a1f1a4e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/NormalBrowserTabViewHolder.kt @@ -0,0 +1,55 @@ +/* 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.tabstray.viewholders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import mozilla.components.concept.tabstray.Tab +import org.mozilla.fenix.R +import org.mozilla.fenix.selection.SelectionHolder +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore +import org.mozilla.fenix.tabstray.browser.BrowserTabsAdapter + +/** + * View holder for the normal tabs tray list. + */ +class NormalBrowserTabViewHolder( + containerView: View, + private val store: TabsTrayStore, + interactor: TabsTrayInteractor, + currentTabIndex: Int +) : BaseBrowserTabViewHolder( + containerView, + store, + interactor, + currentTabIndex +), SelectionHolder { + + /** + * Holds the list of selected tabs. + * + * Implementation notes: we do this here because we only want the normal tabs list to be able + * to select tabs. + */ + override val selectedItems: Set + get() = store.state.mode.selectedTabs + + override val emptyStringText: String + get() = itemView.resources.getString(R.string.no_open_tabs_description) + + override fun bind( + adapter: RecyclerView.Adapter, + layoutManager: RecyclerView.LayoutManager + ) { + (adapter as BrowserTabsAdapter).selectionHolder = this + + super.bind(adapter, layoutManager) + } + + companion object { + const val LAYOUT_ID = R.layout.normal_browser_tray_list + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserTabViewHolder.kt new file mode 100644 index 000000000..b49bc8fc1 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/PrivateBrowserTabViewHolder.kt @@ -0,0 +1,32 @@ +/* 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.tabstray.viewholders + +import android.view.View +import org.mozilla.fenix.R +import org.mozilla.fenix.tabstray.TabsTrayInteractor +import org.mozilla.fenix.tabstray.TabsTrayStore + +/** + * View holder for the private tabs tray list. + */ +class PrivateBrowserTabViewHolder( + containerView: View, + store: TabsTrayStore, + interactor: TabsTrayInteractor, + currentTabIndex: Int +) : BaseBrowserTabViewHolder( + containerView, + store, + interactor, + currentTabIndex +) { + override val emptyStringText: String + get() = itemView.resources.getString(R.string.no_private_tabs_description) + + companion object { + const val LAYOUT_ID = R.layout.private_browser_tray_list + } +} diff --git a/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt new file mode 100644 index 000000000..066fce9fa --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/tabstray/viewholders/SyncedTabViewHolder.kt @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.tabstray.viewholders + +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import kotlinx.android.synthetic.main.component_sync_tabs_tray_layout.* +import org.mozilla.fenix.R +import org.mozilla.fenix.tabstray.TabsTrayStore + +class SyncedTabViewHolder( + containerView: View, + private val tabsTrayStore: TabsTrayStore +) : AbstractTrayViewHolder(containerView) { + + override fun bind( + adapter: RecyclerView.Adapter, + layoutManager: RecyclerView.LayoutManager + ) { + synced_tabs_list.layoutManager = layoutManager + synced_tabs_list.adapter = adapter + + synced_tabs_tray_layout.tabsTrayStore = tabsTrayStore + } + + companion object { + const val LAYOUT_ID = R.layout.component_sync_tabs_tray_layout + } +} 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 4ce4c635c..e6f2aa698 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -26,6 +26,7 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.home.HomeFragment /** @@ -104,7 +105,8 @@ class DefaultTabTrayController( override fun handleNewTabTapped(private: Boolean) { val startTime = profiler?.getProfilerTime() browsingModeManager.mode = BrowsingMode.fromBoolean(private) - navController.navigate(TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) + navController.navigateBlockingForAsyncNavGraph( + TabTrayDialogFragmentDirections.actionGlobalHome(focusOnAddressBar = true)) dismissTabTray() profiler?.addMarker( "DefaultTabTrayController.onNewTabTapped", @@ -113,7 +115,8 @@ class DefaultTabTrayController( } override fun handleTabSettingsClicked() { - navController.navigate(TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()) + navController.navigateBlockingForAsyncNavGraph( + TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()) } override fun handleTabTrayDismissed() { @@ -148,7 +151,7 @@ class DefaultTabTrayController( val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment( data = data.toTypedArray() ) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } override fun handleShareSelectedTabsClicked(selectedTabs: Set) { @@ -158,7 +161,7 @@ class DefaultTabTrayController( val directions = TabTrayDialogFragmentDirections.actionGlobalShareFragment( data = data.toTypedArray() ) - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) } override fun handleBookmarkSelectedTabs(selectedTabs: Set) { @@ -236,13 +239,13 @@ class DefaultTabTrayController( override fun handleRecentlyClosedClicked() { val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed() - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) metrics.track(Event.RecentlyClosedTabsOpened) } override fun handleGoToTabsSettingClicked() { val directions = TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment() - navController.navigate(directions) + navController.navigateBlockingForAsyncNavGraph(directions) metrics.track(Event.TabsTrayCfrTapped) } } 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 ef6f3fe70..0c650171d 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -50,12 +50,14 @@ import mozilla.components.support.utils.ext.right import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R +import org.mozilla.fenix.collections.CollectionsListAdapter import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getDefaultCollectionNumber +import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings @@ -326,7 +328,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler private fun dismissTabTrayAndNavigateHome(sessionId: String) { homeViewModel.sessionToDelete = sessionId val directions = NavGraphDirections.actionGlobalHome() - findNavController().navigate(directions) + findNavController().navigateBlockingForAsyncNavGraph(directions) dismissAllowingStateLoss() } @@ -339,7 +341,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler dismissAllowingStateLoss() if (findNavController().currentDestination?.id == R.id.browserFragment) return if (!findNavController().popBackStack(R.id.browserFragment, false)) { - findNavController().navigate(R.id.browserFragment) + findNavController().navigateBlockingForAsyncNavGraph(R.id.browserFragment) } } @@ -374,7 +376,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler .setText(requireContext().getString(messageStringRes)) .setAction(requireContext().getString(R.string.create_collection_view)) { dismissAllowingStateLoss() - findNavController().navigate( + findNavController().navigateBlockingForAsyncNavGraph( TabTrayDialogFragmentDirections.actionGlobalHome( focusOnAddressBar = false, focusOnCollection = collectionToSelect ?: -1L @@ -398,7 +400,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler .setText(requireContext().getString(R.string.snackbar_message_bookmarks_saved)) .setAction(requireContext().getString(R.string.snackbar_message_bookmarks_view)) { dismissAllowingStateLoss() - findNavController().navigate( + findNavController().navigateBlockingForAsyncNavGraph( TabTrayDialogFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) ) } @@ -428,7 +430,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler .setView(customLayout) .setPositiveButton(android.R.string.ok) { dialog, _ -> val selectedCollection = - (list.adapter as CollectionsAdapter).getSelectedCollection() + (list.adapter as CollectionsListAdapter).getSelectedCollection() val collection = tabCollectionStorage.cachedTabCollections[selectedCollection] viewLifecycleOwner.lifecycleScope.launch(Main) { tabCollectionStorage.addTabsToCollection(collection, sessionList) @@ -448,7 +450,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler val dialog = builder.create() val adapter = - CollectionsAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) { + CollectionsListAdapter(arrayOf(it.getString(R.string.tab_tray_add_new_collection)) + collections) { dialog.dismiss() showAddNewCollectionDialog(sessionList) } diff --git a/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt new file mode 100644 index 000000000..ec86ae2f4 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryLifecycleObserver.kt @@ -0,0 +1,76 @@ +/* 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.telemetry + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.OnLifecycleEvent +import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.support.base.android.Clock +import org.mozilla.fenix.GleanMetrics.EngineTab as EngineMetrics +import org.mozilla.fenix.GleanMetrics.EngineTab.foregroundMetricsKeys as MetricsKeys + +/** + * [LifecycleObserver] to used on the process lifecycle to measure the amount of tabs getting killed + * while the app is in the background. + * + * See: + * - https://github.com/mozilla-mobile/android-components/issues/9624 + * - https://github.com/mozilla-mobile/android-components/issues/9997 + */ +class TelemetryLifecycleObserver( + private val store: BrowserStore +) : LifecycleObserver { + private var pausedState: TabState? = null + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + fun onPause() { + pausedState = createTabState() + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + fun onResume() { + val lastState = pausedState ?: return + val currentState = createTabState() + + EngineMetrics.foregroundMetrics.record(mapOf( + MetricsKeys.backgroundActiveTabs to lastState.activeEngineTabs.toString(), + MetricsKeys.backgroundCrashedTabs to lastState.crashedTabs.toString(), + MetricsKeys.backgroundTotalTabs to lastState.totalTabs.toString(), + MetricsKeys.foregroundActiveTabs to currentState.activeEngineTabs.toString(), + MetricsKeys.foregroundCrashedTabs to currentState.crashedTabs.toString(), + MetricsKeys.foregroundTotalTabs to currentState.totalTabs.toString(), + MetricsKeys.timeInBackground to (currentState.timestamp - lastState.timestamp).toString() + )) + + pausedState = null + } + + private fun createTabState(): TabState { + val tabsWithEngineSession = store.state.tabs + .filter { tab -> tab.engineState.engineSession != null } + .filter { tab -> !tab.engineState.crashed } + .count() + + val totalTabs = store.state.tabs.count() + + val crashedTabs = store.state.tabs + .filter { tab -> tab.engineState.crashed } + .count() + + return TabState( + activeEngineTabs = tabsWithEngineSession, + totalTabs = totalTabs, + crashedTabs = crashedTabs + ) + } +} + +private data class TabState( + val timestamp: Long = Clock.elapsedRealtime(), + val totalTabs: Int, + val crashedTabs: Int, + val activeEngineTabs: Int +) diff --git a/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt similarity index 99% rename from app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt rename to app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt index d4dad123e..0441a9a32 100644 --- a/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/telemetry/TelemetryMiddleware.kt @@ -2,7 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -package org.mozilla.fenix +package org.mozilla.fenix.telemetry import androidx.annotation.VisibleForTesting import mozilla.components.browser.state.action.BrowserAction 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 6c4801216..c67a40e32 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -33,13 +33,17 @@ import org.mozilla.fenix.components.metrics.MozillaProductDetector import org.mozilla.fenix.components.settings.counterPreference import org.mozilla.fenix.components.settings.featureFlagPreference import org.mozilla.fenix.components.toolbar.ToolbarPosition +import org.mozilla.fenix.experiments.ExperimentBranch +import org.mozilla.fenix.experiments.Experiments import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.getPreferenceKey +import org.mozilla.fenix.ext.withExperiment import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.deletebrowsingdata.DeleteBrowsingDataOnQuitType import org.mozilla.fenix.settings.logins.SavedLoginsSortingStrategyMenu import org.mozilla.fenix.settings.logins.SortingStrategy import org.mozilla.fenix.settings.registerOnSharedPreferenceChangeListener +import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL import java.security.InvalidParameterException private const val AUTOPLAY_USER_SETTING = "AUTOPLAY_USER_SETTING" @@ -60,6 +64,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 APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD = 3 const val ONE_DAY_MS = 60 * 60 * 24 * 1000L const val THREE_DAYS_MS = 3 * ONE_DAY_MS @@ -291,6 +296,31 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + /** + * Shows if the user has chosen to close the set default browser experiment card + * on home screen or has clicked the set as default browser button. + */ + var userDismissedExperimentCard by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_experiment_card_home), + default = false + ) + + /** + * Shows if the set default browser experiment card should be shown on home screen. + */ + fun shouldShowSetAsDefaultBrowserCard(): Boolean { + val browsers = BrowsersCache.all(appContext) + val experiments = appContext.components.analytics.experiments + val isExperimentBranch = + experiments.withExperiment(Experiments.DEFAULT_BROWSER) { experimentBranch -> + (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_NEW_TAB_BANNER) + } + return isExperimentBranch && + !userDismissedExperimentCard && + !browsers.isFirefoxDefaultBrowser && + numberOfAppLaunches > APP_LAUNCHES_TO_SHOW_DEFAULT_BROWSER_CARD + } + var listTabView by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_tab_view_list), default = true @@ -323,7 +353,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { var tabsTrayRewrite by featureFlagPreference( appContext.getPreferenceKey(R.string.pref_key_new_tabs_tray), - default = false, + default = true, featureFlag = FeatureFlags.tabsTrayRewrite ) @@ -754,9 +784,7 @@ class Settings(private val appContext: Context) : PreferencesHolder { * either [AUTOPLAY_ALLOW_ALL] or [AUTOPLAY_BLOCK_ALL]. Because of this, we are forced to save * the user selected setting as well. */ - fun getAutoplayUserSetting( - default: Int - ) = preferences.getInt(AUTOPLAY_USER_SETTING, default) + fun getAutoplayUserSetting() = preferences.getInt(AUTOPLAY_USER_SETTING, AUTOPLAY_BLOCK_ALL) private fun getSitePermissionsPhoneFeatureAutoplayAction( feature: PhoneFeature, @@ -833,11 +861,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = true ) - var fxaSignedIn by booleanPreference( - appContext.getPreferenceKey(R.string.pref_key_fxa_signed_in), - default = false - ) - var fxaHasSyncedItems by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_fxa_has_synced_items), default = false @@ -898,6 +921,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false ) + var allowDomesticChinaFxaServer by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_allow_domestic_china_fxa_server), + default = true + ) + var overrideFxAServer by stringPreference( appContext.getPreferenceKey(R.string.pref_key_override_fxa_server), default = "" @@ -1040,4 +1068,13 @@ class Settings(private val appContext: Context) : PreferencesHolder { default = false, featureFlag = FeatureFlags.addressesFeature ) + + /** + * Storing desktop item checkbox value in the home screen menu. + * If set to true, next opened tab from home screen will be opened in desktop mode. + */ + var openNextTabInDesktopMode by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_open_next_tab_desktop_mode), + default = false + ) } diff --git a/app/src/main/java/org/mozilla/fenix/utils/view/LifecycleViewProvider.kt b/app/src/main/java/org/mozilla/fenix/utils/view/LifecycleViewProvider.kt index 010f82b6f..b06c46768 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/view/LifecycleViewProvider.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/view/LifecycleViewProvider.kt @@ -14,7 +14,7 @@ import androidx.lifecycle.LifecycleRegistry /** * Provides a [LifecycleOwner] on a given [View] for features that function on lifecycle events. * - * When the [View] is attached to the window, observers will receive the [Lifecycle.Event.ON_START] event. + * When the [View] is attached to the window, observers will receive the [Lifecycle.Event.ON_RESUME] event. * When the [View] is detached to the window, observers will receive the [Lifecycle.Event.ON_STOP] event. * * @param view The [View] that will be observed. @@ -36,7 +36,7 @@ internal class ViewBinding( private val registry: LifecycleRegistry ) : View.OnAttachStateChangeListener { override fun onViewAttachedToWindow(v: View?) { - registry.currentState = State.STARTED + registry.currentState = State.RESUMED } override fun onViewDetachedFromWindow(v: View?) { diff --git a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt index eec7d25e6..603c76bdc 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/SitePermissionsWifiIntegration.kt @@ -4,11 +4,12 @@ package org.mozilla.fenix.wifi -import mozilla.components.feature.sitepermissions.SitePermissionsRules +import androidx.annotation.VisibleForTesting +import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.ALLOWED +import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action.BLOCKED import mozilla.components.support.base.feature.LifecycleAwareFeature import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_ALLOW_ON_WIFI -import org.mozilla.fenix.settings.sitepermissions.AUTOPLAY_BLOCK_ALL import org.mozilla.fenix.utils.Settings /** @@ -24,11 +25,11 @@ class SitePermissionsWifiIntegration( * Adds listener for autoplay setting [AUTOPLAY_ALLOW_ON_WIFI]. Sets all autoplay to allowed when * WIFI is connected, blocked otherwise. */ - private val wifiConnectedListener: ((Boolean) -> Unit) by lazy { + @VisibleForTesting + internal val wifiConnectedListener: ((Boolean) -> Unit) by lazy { { connected: Boolean -> - val setting = - if (connected) SitePermissionsRules.Action.ALLOWED else SitePermissionsRules.Action.BLOCKED - if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { + if (settings.getAutoplayUserSetting() == AUTOPLAY_ALLOW_ON_WIFI) { + val setting = if (connected) ALLOWED else BLOCKED settings.setSitePermissionsPhoneFeatureAction( PhoneFeature.AUTOPLAY_AUDIBLE, setting @@ -39,21 +40,11 @@ class SitePermissionsWifiIntegration( ) } else { // The autoplay setting has changed, we can remove the listener - removeWifiConnectedListener() + stop() } } } - /** - * If autoplay is only enabled on WIFI, sets a WIFI listener to set them accordingly. Otherwise - * noop. - */ - fun maybeAddWifiConnectedListener() { - if (settings.getAutoplayUserSetting(default = AUTOPLAY_BLOCK_ALL) == AUTOPLAY_ALLOW_ON_WIFI) { - addWifiConnectedListener() - } - } - fun addWifiConnectedListener() { wifiConnectionMonitor.addOnWifiConnectedChangedListener(wifiConnectedListener) } @@ -62,15 +53,15 @@ class SitePermissionsWifiIntegration( wifiConnectionMonitor.removeOnWifiConnectedChangedListener(wifiConnectedListener) } - // Until https://bugzilla.mozilla.org/show_bug.cgi?id=1621825 is fixed, AUTOPLAY_ALLOW_ALL - // only works while WIFI is active, so we are not using AUTOPLAY_ALLOW_ON_WIFI (or this class). - // Once that is fixed, [start] and [maybeAddWifiConnectedListener] will need to be called on - // activity startup. override fun start() { - wifiConnectionMonitor.start() + if (settings.getAutoplayUserSetting() == AUTOPLAY_ALLOW_ON_WIFI) { + wifiConnectionMonitor.start() + addWifiConnectedListener() + } } override fun stop() { wifiConnectionMonitor.stop() + removeWifiConnectedListener() } } diff --git a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt index 4db646e12..682a2f1e0 100644 --- a/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt +++ b/app/src/main/java/org/mozilla/fenix/wifi/WifiConnectionMonitor.kt @@ -10,6 +10,7 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest +import androidx.annotation.VisibleForTesting /** * Attaches itself to the [Application] and listens for WIFI available/not available events. This @@ -26,20 +27,28 @@ import android.net.NetworkRequest * ``` */ class WifiConnectionMonitor(app: Application) { - private val callbacks = mutableListOf<(Boolean) -> Unit>() - private val connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as + @VisibleForTesting + internal val callbacks = mutableListOf<(Boolean) -> Unit>() + + @VisibleForTesting + internal var connectivityManager = app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - private var lastKnownStateWasAvailable: Boolean? = null - private var isRegistered = false + @VisibleForTesting + internal var lastKnownStateWasAvailable: Boolean? = null + + @VisibleForTesting + internal var isRegistered = false + @Synchronized set - private val frameworkListener = object : ConnectivityManager.NetworkCallback() { - override fun onLost(network: Network?) { + @VisibleForTesting + internal val frameworkListener = object : ConnectivityManager.NetworkCallback() { + override fun onLost(network: Network) { notifyListeners(false) lastKnownStateWasAvailable = false } - override fun onAvailable(network: Network?) { + override fun onAvailable(network: Network) { notifyListeners(true) lastKnownStateWasAvailable = true } @@ -86,6 +95,8 @@ class WifiConnectionMonitor(app: Application) { if (!isRegistered) return connectivityManager.unregisterNetworkCallback(frameworkListener) isRegistered = false + lastKnownStateWasAvailable = null + callbacks.clear() } /** diff --git a/app/src/main/res/animator/fill_disable.xml b/app/src/main/res/animator/fill_disable.xml deleted file mode 100644 index c0887a6ac..000000000 --- a/app/src/main/res/animator/fill_disable.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/app/src/main/res/animator/fill_enable.xml b/app/src/main/res/animator/fill_enable.xml deleted file mode 100644 index 52d745f77..000000000 --- a/app/src/main/res/animator/fill_enable.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/app/src/main/res/animator/strike_thru_mask_disable.xml b/app/src/main/res/animator/strike_thru_mask_disable.xml deleted file mode 100644 index 09049a7b6..000000000 --- a/app/src/main/res/animator/strike_thru_mask_disable.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/app/src/main/res/animator/strike_thru_mask_enable.xml b/app/src/main/res/animator/strike_thru_mask_enable.xml deleted file mode 100644 index e45b8c676..000000000 --- a/app/src/main/res/animator/strike_thru_mask_enable.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - diff --git a/app/src/main/res/animator/strike_thru_path_disable.xml b/app/src/main/res/animator/strike_thru_path_disable.xml deleted file mode 100644 index b56cca0e2..000000000 --- a/app/src/main/res/animator/strike_thru_path_disable.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/animator/strike_thru_path_enable.xml b/app/src/main/res/animator/strike_thru_path_enable.xml deleted file mode 100644 index 8ea41146c..000000000 --- a/app/src/main/res/animator/strike_thru_path_enable.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable-v24/shield_dark.xml b/app/src/main/res/drawable-v24/shield_dark.xml index a40e505f4..0e322f569 100644 --- a/app/src/main/res/drawable-v24/shield_dark.xml +++ b/app/src/main/res/drawable-v24/shield_dark.xml @@ -13,23 +13,27 @@ android:viewportHeight="24"> + + android:pathData="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z"> + android:endX="3" + android:endY="22" + android:startX="21" + android:startY="2" + android:type="linear"> + + + @@ -41,8 +45,8 @@ android:duration="330" android:interpolator="@android:interpolator/fast_out_slow_in" android:propertyName="pathData" - android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z" - android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z" + android:valueFrom="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" + android:valueTo="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" android:valueType="pathType" /> + + android:pathData="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z"> + android:endX="3" + android:endY="22" + android:startX="21" + android:startY="2" + android:type="linear"> + + + @@ -41,8 +45,8 @@ android:duration="330" android:interpolator="@android:interpolator/fast_out_slow_in" android:propertyName="pathData" - android:valueFrom="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z" - android:valueTo="M 20 6 C 20 5 19.2 4.1 18.2 4 L 12 3 L 5.8 4 C 4.8 4 4 5 4 6 L 4.1 11 C 4.4 14.2 5.1 16 6.6 18 C 7.929 19.627 9.821 20.698 11.9 21 L 12.1 21 C 14.2 20.7 16.1 19.6 17.4 18 C 19 16 19.6 14.2 19.9 11 L 20 6 Z M 17.9 10.8 C 17.9 12.963 17.198 15.069 15.9 16.8 C 14.9 17.9 13.5 18.8 12 19.1 C 10.452 18.8 9.066 17.946 8.1 16.7 C 6.816 15 6.114 12.93 6.1 10.8 C 6.041 9.167 6.041 7.533 6.1 5.9 L 12 5 L 17.9 6 L 18 6.2 L 17.9 10.9 Z M 8 7.6 L 8 10.6 C 8.3 13.3 8.8 14.3 9.7 15.6 C 10.3 16.2 11.1 16.8 12 17 L 12 7 L 8 7.6 Z" + android:valueFrom="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" + android:valueTo="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" android:valueType="pathType" /> + android:pathData="m15.535,10.526 l1.6,1.6a0.5,0.5 0,0 1,-0.351 0.854l-4.233,0.02 -0.551,-0.551 0.021,-4.232a0.5,0.5 0,0 1,0.854 -0.351l1.596,1.596L19.72,4.22a0.75,0.75 0,1 1,1.061 1.061l-5.246,5.245zM7.6,14H6.4l-0.4,0.4v1.2l0.4,0.4h1.2l0.4,-0.4v-1.2l-0.4,-0.4zM10.6,14H9.4l-0.4,0.4v1.2l0.4,0.4h1.2l0.4,-0.4v-1.2l-0.4,-0.4zM13.6,14h-1.2l-0.4,0.4v1.2l0.4,0.4h1.2l0.4,-0.4v-1.2l-0.4,-0.4zM10.6,11H9.4l-0.4,0.4v1.2l0.4,0.4h1.2l0.4,-0.4v-1.2l-0.4,-0.4z" + android:fillColor="?primaryText"/> + diff --git a/app/src/main/res/drawable/ic_addons_extensions.xml b/app/src/main/res/drawable/ic_addons_extensions.xml index 5fabf10b2..efe803ba3 100644 --- a/app/src/main/res/drawable/ic_addons_extensions.xml +++ b/app/src/main/res/drawable/ic_addons_extensions.xml @@ -8,6 +8,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_alert.xml b/app/src/main/res/drawable/ic_alert.xml index 1546d32e6..94ab8c95e 100644 --- a/app/src/main/res/drawable/ic_alert.xml +++ b/app/src/main/res/drawable/ic_alert.xml @@ -8,6 +8,12 @@ android:viewportWidth="24" android:viewportHeight="24"> - + android:fillColor="@color/sync_error_text_color" + android:pathData="M11.75,7C11.336,7 11,7.336 11,7.75V13.25C11,13.664 11.336,14 11.75,14C12.164,14 12.5,13.664 12.5,13.25V7.75C12.5,7.336 12.164,7 11.75,7Z" /> + + + diff --git a/app/src/main/res/drawable/ic_arrowhead_right.xml b/app/src/main/res/drawable/ic_arrowhead_right.xml index c899ca92f..268da1cce 100644 --- a/app/src/main/res/drawable/ic_arrowhead_right.xml +++ b/app/src/main/res/drawable/ic_arrowhead_right.xml @@ -5,9 +5,9 @@ android:width="24dp" android:height="24dp" android:autoMirrored="true" - android:viewportWidth="7" - android:viewportHeight="13"> + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M14.289,12 L7.57,5.28a0.75,0.75 0,1 1,1.061 -1.061l7.37,7.37v0.821l-7.37,7.37a0.748,0.748 0,0 1,-1.061 0,0.75 0.75,0 0,1 0,-1.061L14.289,12z" /> diff --git a/app/src/main/res/drawable/ic_autoplay_disabled.xml b/app/src/main/res/drawable/ic_autoplay_disabled.xml index d4416eefb..4d069cf50 100644 --- a/app/src/main/res/drawable/ic_autoplay_disabled.xml +++ b/app/src/main/res/drawable/ic_autoplay_disabled.xml @@ -9,11 +9,8 @@ android:viewportHeight="24"> + android:pathData="M3.326 4.265A0.83 0.83 0 0 1 3.225 4h-0.36C2.842 4 2.822 4.008 2.8 4.01a0.737 0.737 0 0 0-0.58 0.21 0.75 0.75 0 0 0 0 1.061l1.847 1.847A9.441 9.441 0 0 0 2.7 12c0 5.238 4.262 9.5 9.5 9.5a9.42 9.42 0 0 0 4.879-1.36l1.64 1.64a0.748 0.748 0 0 0 1.06 0 0.75 0.75 0 0 0 0-1.061L3.326 4.265zM12.2 20c-4.411 0-8-3.589-8-8 0-1.328 0.352-2.609 0.975-3.764l4.301 4.301v3.026a0.89 0.89 0 0 0 1.333 0.773l1.569-0.897 3.601 3.601A7.918 7.918 0 0 1 12.2 20zM8 4.364L7.636 4H5.14L8 6.86V4.364zM12.2 4c2.137 0 4.146 0.832 5.657 2.343A7.948 7.948 0 0 1 20.2 12a7.964 7.964 0 0 1-1.898 5.162l1.06 1.06A9.45 9.45 0 0 0 21.7 12a9.442 9.442 0 0 0-2.782-6.718A9.442 9.442 0 0 0 12.2 2.5a0.75 0.75 0 0 0 0 1.5z" /> - + android:pathData="M17.046 12.773a0.89 0.89 0 0 0 0-1.547L10.81 7.664a0.887 0.887 0 0 0-1.322 0.685l5.564 5.564 1.994-1.14z" /> diff --git a/app/src/main/res/drawable/ic_autoplay_enabled.xml b/app/src/main/res/drawable/ic_autoplay_enabled.xml index 424118209..fa534a57c 100644 --- a/app/src/main/res/drawable/ic_autoplay_enabled.xml +++ b/app/src/main/res/drawable/ic_autoplay_enabled.xml @@ -5,5 +5,10 @@ - + + diff --git a/app/src/main/res/drawable/ic_bookmark_filled.xml b/app/src/main/res/drawable/ic_bookmark_filled.xml index 090a843c0..8989f5484 100644 --- a/app/src/main/res/drawable/ic_bookmark_filled.xml +++ b/app/src/main/res/drawable/ic_bookmark_filled.xml @@ -9,6 +9,6 @@ android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_bookmark_list.xml b/app/src/main/res/drawable/ic_bookmark_list.xml index eec7c8ffe..9937d1a68 100644 --- a/app/src/main/res/drawable/ic_bookmark_list.xml +++ b/app/src/main/res/drawable/ic_bookmark_list.xml @@ -10,6 +10,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="m7.225,11.324 l-2.86,-2.375a1.207,1.207 0,0 1,-0.378 -1.309,1.205 1.205,0 0,1 1.074,-0.835l3.697,-0.239 0.501,-0.375 1.364,-3.427A1.209,1.209 0,0 1,11.75 2c0.499,0 0.941,0.3 1.127,0.763v0.001l1.364,3.427 0.501,0.375 3.697,0.239c0.498,0.032 0.92,0.36 1.074,0.835 0.155,0.476 0.006,0.99 -0.378,1.309L16.28,11.32l-0.18,0.592 0.91,3.575c0.123,0.485 -0.059,0.986 -0.463,1.279a1.202,1.202 0,0 1,-1.359 0.046l-3.127,-1.977h-0.621l-3.126,1.977c-0.426,0.266 -0.96,0.247 -1.361,-0.045a1.205,1.205 0,0 1,-0.463 -1.279l0.908,-3.566 -0.173,-0.598z" + android:fillColor="?primaryText"/> + diff --git a/app/src/main/res/drawable/ic_bookmark_outline.xml b/app/src/main/res/drawable/ic_bookmark_outline.xml index da59f26a6..67c872b2b 100644 --- a/app/src/main/res/drawable/ic_bookmark_outline.xml +++ b/app/src/main/res/drawable/ic_bookmark_outline.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M16.6,21.1a2,2 0,0 1,-1.2 -0.3l-3.3,-2H12l-3.3,2c-0.8,0.5 -1.7,0.4 -2.4,0a2,2 0,0 1,-0.8 -2.3l1,-3.8 -0.1,-0.2 -3,-2.5a2,2 0,0 1,-0.7 -2.3,2 2,0 0,1 1.9,-1.5L8.4,8l0.2,-0.2L10,4.2a2.1,2.1 0,0 1,2 -1.3c0.9,0 1.6,0.5 2,1.3l1.4,3.6 0.2,0.2 4,0.2a2,2 0,0 1,1.8 1.5,2 2,0 0,1 -0.7,2.3l-3,2.5v0.2l1,3.8a2,2 0,0 1,-0.9 2.2,2 2,0 0,1 -1.2,0.4zM12,4.4a0.6,0.6 0,0 0,-0.6 0.3l-1.6,4 -0.9,0.8 -4.3,0.2a0.6,0.6 0,0 0,-0.6 0.5,0.6 0.6,0 0,0 0.2,0.6l3.4,2.8 0.3,1L7,19a0.6,0.6 0,0 0,0.2 0.6,0.6 0.6,0 0,0 0.7,0l3.6,-2.3h1.2l3.6,2.3c0.2,0.2 0.5,0.2 0.7,0a0.6,0.6 0,0 0,0.3 -0.6L16,14.7l0.3,-1.1 3.4,-2.8a0.6,0.6 0,0 0,0.2 -0.6,0.6 0.6,0 0,0 -0.6,-0.5l-4.3,-0.2 -1,-0.7 -1.5,-4a0.6,0.6 0,0 0,-0.6 -0.4z" /> diff --git a/app/src/main/res/drawable/ic_bookmarks_menu.xml b/app/src/main/res/drawable/ic_bookmarks_menu.xml new file mode 100644 index 000000000..fce6d6829 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmarks_menu.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml index 00ef35e6a..9c5bca788 100644 --- a/app/src/main/res/drawable/ic_camera.xml +++ b/app/src/main/res/drawable/ic_camera.xml @@ -10,13 +10,4 @@ - - - diff --git a/app/src/main/res/drawable/ic_camera_anim_disable.xml b/app/src/main/res/drawable/ic_camera_anim_disable.xml deleted file mode 100644 index 8cf218a7d..000000000 --- a/app/src/main/res/drawable/ic_camera_anim_disable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_camera_anim_enable.xml b/app/src/main/res/drawable/ic_camera_anim_enable.xml deleted file mode 100644 index 4cf33171a..000000000 --- a/app/src/main/res/drawable/ic_camera_anim_enable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_camera_disabled.xml b/app/src/main/res/drawable/ic_camera_disabled.xml index ce49afbc2..6802c51fa 100644 --- a/app/src/main/res/drawable/ic_camera_disabled.xml +++ b/app/src/main/res/drawable/ic_camera_disabled.xml @@ -3,25 +3,12 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_camera_enabled.xml b/app/src/main/res/drawable/ic_camera_enabled.xml index 7eead7c05..3818f98e4 100644 --- a/app/src/main/res/drawable/ic_camera_enabled.xml +++ b/app/src/main/res/drawable/ic_camera_enabled.xml @@ -7,21 +7,8 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - + diff --git a/app/src/main/res/drawable/ic_chevron_down.xml b/app/src/main/res/drawable/ic_chevron_down.xml index aef6a00b1..6177fe8a1 100644 --- a/app/src/main/res/drawable/ic_chevron_down.xml +++ b/app/src/main/res/drawable/ic_chevron_down.xml @@ -3,10 +3,10 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M12 14.289L5.28 7.57a0.75 0.75 0 1 0-1.061 1.061l7.37 7.37h0.821l7.37-7.37a0.748 0.748 0 0 0 0-1.061 0.75 0.75 0 0 0-1.061 0L12 14.289z"/> diff --git a/app/src/main/res/drawable/ic_chevron_up.xml b/app/src/main/res/drawable/ic_chevron_up.xml index 9148fa55d..60d8c82f8 100644 --- a/app/src/main/res/drawable/ic_chevron_up.xml +++ b/app/src/main/res/drawable/ic_chevron_up.xml @@ -3,10 +3,10 @@ - License, v. 2.0. If a copy of the MPL was not distributed with this - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="m12,8.711 l-6.72,6.72a0.75,0.75 0,1 1,-1.061 -1.061L11.589,7h0.821l7.37,7.37c0.147,0.146 0.22,0.338 0.22,0.53a0.75,0.75 0,0 1,-1.281 0.53L12,8.711z"/> diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml index 04435d15c..049b0b031 100644 --- a/app/src/main/res/drawable/ic_clear.xml +++ b/app/src/main/res/drawable/ic_clear.xml @@ -5,9 +5,9 @@ + android:viewportWidth="20" + android:viewportHeight="20"> + android:pathData="M10,2.5C5.858,2.5 2.5,5.858 2.5,10C2.5,14.142 5.858,17.5 10,17.5C14.142,17.5 17.5,14.142 17.5,10C17.5,5.858 14.142,2.5 10,2.5ZM12.817,11.933C13.06,12.177 13.06,12.574 12.817,12.818C12.695,12.939 12.535,13 12.375,13C12.215,13 12.055,12.939 11.933,12.817L10.204,11.088L9.797,11.087L8.068,12.816C7.945,12.939 7.785,13 7.625,13C7.465,13 7.305,12.939 7.183,12.817C6.94,12.573 6.94,12.176 7.183,11.932L8.897,10.218V9.781L7.183,8.067C6.94,7.823 6.94,7.426 7.183,7.182C7.427,6.938 7.824,6.938 8.068,7.182L9.787,8.901H10.215L11.934,7.182C12.178,6.938 12.575,6.938 12.819,7.182C13.062,7.426 13.062,7.823 12.819,8.067L11.1,9.786V10.214L12.817,11.933Z" /> diff --git a/app/src/main/res/drawable/ic_close.xml b/app/src/main/res/drawable/ic_close.xml index e2c2ea022..7b82c9846 100644 --- a/app/src/main/res/drawable/ic_close.xml +++ b/app/src/main/res/drawable/ic_close.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M13.108 11.776l4.709-4.709c0.244-0.244 0.244-0.641 0-0.885s-0.64-0.244-0.884 0l-4.689 4.689h-0.488L7.067 6.183c-0.244-0.244-0.64-0.244-0.884 0s-0.244 0.641 0 0.885l4.687 4.686v0.491l-4.687 4.687c-0.244 0.244-0.244 0.641 0 0.885C6.305 17.939 6.465 18 6.625 18c0.16 0 0.32-0.061 0.442-0.183l4.687-4.687h0.491l4.687 4.687C17.055 17.939 17.215 18 17.375 18c0.16 0 0.32-0.061 0.442-0.183 0.244-0.244 0.244-0.641 0-0.885l-4.709-4.709v-0.447z" /> diff --git a/app/src/main/res/drawable/ic_confirm_email.xml b/app/src/main/res/drawable/ic_confirm_email.xml new file mode 100644 index 000000000..19fa069b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_confirm_email.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_cookies.xml b/app/src/main/res/drawable/ic_cookies.xml index 553f16518..5c2e0c053 100644 --- a/app/src/main/res/drawable/ic_cookies.xml +++ b/app/src/main/res/drawable/ic_cookies.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M20.862,11.69a7.886,7.886 0,0 1,-1.856 0.244L19,11.934v0.817l-0.5,0.5L17,13.251l-0.5,-0.5L16.5,11.53c-3.193,-1.052 -5.501,-4.055 -5.501,-7.6 0,-0.296 0.023,-0.585 0.055,-0.87l-0.565,-0.499c-4.531,0.725 -7.997,4.64 -7.997,9.375A9.508,9.508 0,0 0,12 21.443c5.146,0 9.327,-4.09 9.492,-9.198l-0.63,-0.556zM5,12.75v-1.5l0.5,-0.5L7,10.75l0.5,0.5v1.5l-0.5,0.5L5.5,13.25l-0.5,-0.5zM9.5,18l-0.5,0.5L7.5,18.5L7,18v-1.5l0.5,-0.5L9,16l0.5,0.5L9.5,18zM9.5,7.5L9,8L7.5,8L7,7.5L7,6l0.5,-0.5L9,5.5l0.5,0.5v1.5zM13.25,12.75 L12.75,13.25h-1.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5v1.5zM17,18l-0.5,0.5L15,18.5l-0.5,-0.5v-1.5l0.5,-0.5h1.5l0.5,0.5L17,18z" /> diff --git a/app/src/main/res/drawable/ic_cryptominers.xml b/app/src/main/res/drawable/ic_cryptominers.xml index 9d0aba381..9764b05a1 100644 --- a/app/src/main/res/drawable/ic_cryptominers.xml +++ b/app/src/main/res/drawable/ic_cryptominers.xml @@ -9,6 +9,5 @@ android:viewportHeight="24"> + android:pathData="M16.5 4h-3V2.75a0.75 0.75 0 0 0-1.5 0V4H9.039C8.631 4 8.24 4.167 7.957 4.461L6.418 6.065A1.499 1.499 0 0 0 6 7.103V7.5A1.5 1.5 0 0 0 7.5 9H12v11.25a0.75 0.75 0 0 0 1.5 0V9h3A1.5 1.5 0 0 0 18 7.5v-2A1.5 1.5 0 0 0 16.5 4z" /> diff --git a/app/src/main/res/drawable/ic_delete.xml b/app/src/main/res/drawable/ic_delete.xml index c8f283867..016f2ad0b 100644 --- a/app/src/main/res/drawable/ic_delete.xml +++ b/app/src/main/res/drawable/ic_delete.xml @@ -8,6 +8,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M20.25,6L16,6L16,4.5A2.5,2.5 0,0 0,13.5 2h-3A2.5,2.5 0,0 0,8 4.5L8,6L3.75,6a0.75,0.75 0,0 0,0 1.5L4,7.5v12A2.5,2.5 0,0 0,6.5 22h11a2.5,2.5 0,0 0,2.5 -2.5v-12h0.25a0.75,0.75 0,0 0,0 -1.5zM9.5,4.3l0.8,-0.8h3.4l0.8,0.8L14.5,6h-5L9.5,4.3zM18.5,19.7 L17.7,20.5L6.3,20.5l-0.8,-0.8L5.5,7.5h13v12.2z" + android:fillColor="?primaryText"/> + diff --git a/app/src/main/res/drawable/ic_desktop.xml b/app/src/main/res/drawable/ic_desktop.xml index 9eec900b6..76629aa8e 100644 --- a/app/src/main/res/drawable/ic_desktop.xml +++ b/app/src/main/res/drawable/ic_desktop.xml @@ -8,5 +8,5 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M20.5,17h-2a2.5,2.5 0,0 0,2.5 -2.5v-9A2.5,2.5 0,0 0,18.5 3h-13A2.5,2.5 0,0 0,3 5.5v9A2.5,2.5 0,0 0,5.5 17h-2a1.5,1.5 0,0 0,0 3h17a1.5,1.5 0,0 0,0 -3zM5.3,15.5l-0.8,-0.8L4.5,5.3l0.8,-0.8h13.4l0.8,0.8v9.4l-0.8,0.8L5.3,15.5zM13.25,18.5h-2.5a0.75,0.75 0,0 1,0 -1.5h2.5a0.75,0.75 0,0 1,0 1.5z" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml index 144122018..ecccd5dd1 100644 --- a/app/src/main/res/drawable/ic_download.xml +++ b/app/src/main/res/drawable/ic_download.xml @@ -9,5 +9,8 @@ android:viewportHeight="24"> + android:pathData="M5.782 20.5l-0.8-0.8v-1.95a0.75 0.75 0 0 0-1.5 0v1.75a2.5 2.5 0 0 0 2.5 2.5h11.535a2.5 2.5 0 0 0 2.5-2.5v-1.75a0.75 0.75 0 0 0-1.5 0v1.95l-0.8 0.8H5.782z" /> + diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml index e92193503..3da6ea077 100644 --- a/app/src/main/res/drawable/ic_edit.xml +++ b/app/src/main/res/drawable/ic_edit.xml @@ -9,5 +9,5 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M20.976 19.308l-0.752-4.055a2.602 2.602 0 0 0-0.719-1.369L8.356 2.735a2.522 2.522 0 0 0-3.559 0L2.735 4.797a2.522 2.522 0 0 0 0 3.559l11.151 11.15c0.375 0.374 0.849 0.622 1.366 0.717l4.056 0.752a1.43 1.43 0 0 0 1.668-1.667zM5.166 8.666L3.642 7.142V6.011l2.369-2.369h1.131l1.524 1.524-3.5 3.5zm10.357 10.082a1.098 1.098 0 0 1-0.577-0.303l-8.72-8.719 3.5-3.5 8.714 8.714c0.161 0.162 0.267 0.363 0.308 0.586l0.625 3.371-0.476 0.476-3.374-0.625z"/> diff --git a/app/src/main/res/drawable/ic_fab_sync.xml b/app/src/main/res/drawable/ic_fab_sync.xml new file mode 100644 index 000000000..52ac21e97 --- /dev/null +++ b/app/src/main/res/drawable/ic_fab_sync.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_fingerprinters.xml b/app/src/main/res/drawable/ic_fingerprinters.xml index 9a4652791..14dd24df1 100644 --- a/app/src/main/res/drawable/ic_fingerprinters.xml +++ b/app/src/main/res/drawable/ic_fingerprinters.xml @@ -9,5 +9,11 @@ android:viewportHeight="24"> - + android:pathData="M6.688 18H2.75a0.75 0.75 0 0 1 0-1.5h3.938c3.392 0 6.15-2.76 6.15-6.151V9.722a0.75 0.75 0 0 1 1.5 0v0.627C14.339 14.567 10.907 18 6.688 18z" /> + + + diff --git a/app/src/main/res/drawable/ic_folder_icon.xml b/app/src/main/res/drawable/ic_folder_icon.xml index 437fe48e0..ea7cea353 100644 --- a/app/src/main/res/drawable/ic_folder_icon.xml +++ b/app/src/main/res/drawable/ic_folder_icon.xml @@ -6,8 +6,8 @@ android:autoMirrored="true" android:width="24dp" android:height="24dp" - android:viewportWidth="20" - android:viewportHeight="17"> + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M18.5 6h-6.354l-2.039-2.199A2.503 2.503 0 0 0 8.274 3H4.5A2.502 2.502 0 0 0 2 5.5v12C2 18.879 3.121 20 4.5 20h14c1.379 0 2.5-1.121 2.5-2.5v-9C21 7.121 19.879 6 18.5 6zm1 11.7l-0.8 0.8H4.3l-0.8-0.8V5.3l0.8-0.8h3.974c0.277 0 0.545 0.117 0.733 0.32l2.146 2.315 0.869 0.365H18.7l0.8 0.8v9.4z" /> diff --git a/app/src/main/res/drawable/ic_folder_new.xml b/app/src/main/res/drawable/ic_folder_new.xml index a8de11332..fc41e1d89 100644 --- a/app/src/main/res/drawable/ic_folder_new.xml +++ b/app/src/main/res/drawable/ic_folder_new.xml @@ -9,5 +9,9 @@ android:viewportHeight="24"> + android:pathData="M21 18.5a0.745 0.745 0 0 0 0.53-0.22A0.75 0.75 0 0 0 21 17h-1.7l-0.8-0.8v-1.7a0.75 0.75 0 0 0-1.5 0v1.7L16.2 17h-1.7a0.75 0.75 0 0 0 0 1.5h1.7l0.8 0.8V21a0.75 0.75 0 0 0 1.5 0v-1.7l0.8-0.8H21z" /> + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml index ca731897c..e15b9ed93 100644 --- a/app/src/main/res/drawable/ic_help.xml +++ b/app/src/main/res/drawable/ic_help.xml @@ -9,5 +9,8 @@ android:viewportHeight="24"> + android:pathData="M11.75 14A0.75 0.75 0 0 1 11 13.25v-0.959c0-0.645 0.452-1.206 1.074-1.336a1.6 1.6 0 0 0 1.27-1.561 1.596 1.596 0 0 0-3.101-0.522 0.75 0.75 0 1 1-1.418-0.49A3.097 3.097 0 0 1 11.75 6.3a3.098 3.098 0 0 1 3.094 3.094c0 1.418-0.979 2.661-2.344 3.002v0.854A0.75 0.75 0 0 1 11.75 14zm0.45 3h-0.9L11 16.7v-0.9l0.3-0.3h0.9l0.3 0.3v0.9L12.2 17z" /> + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml index aa49fa9ec..757559a7e 100644 --- a/app/src/main/res/drawable/ic_history.xml +++ b/app/src/main/res/drawable/ic_history.xml @@ -9,5 +9,8 @@ android:viewportHeight="24"> + android:pathData="M11.974 21.5c-5.238 0-9.5-4.262-9.5-9.5s4.262-9.5 9.5-9.5 9.5 4.262 9.5 9.5-4.262 9.5-9.5 9.5zm0-17.5c-4.411 0-8 3.589-8 8s3.589 8 8 8 8-3.589 8-8-3.589-8-8-8z" /> + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml index 23964049b..0897d1761 100644 --- a/app/src/main/res/drawable/ic_info.xml +++ b/app/src/main/res/drawable/ic_info.xml @@ -8,6 +8,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M11.75,17a0.75,0.75 0,0 1,-0.75 -0.75v-5.5a0.75,0.75 0,0 1,1.5 0v5.5a0.75,0.75 0,0 1,-0.75 0.75zM12.2,7h-0.9l-0.3,0.3v0.9l0.3,0.3h0.9l0.3,-0.3v-0.9l-0.3,-0.3z" + android:fillColor="?primaryText"/> + diff --git a/app/src/main/res/drawable/ic_internet.xml b/app/src/main/res/drawable/ic_internet.xml index 7aea69447..f6276035e 100644 --- a/app/src/main/res/drawable/ic_internet.xml +++ b/app/src/main/res/drawable/ic_internet.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M12 2.5A9.5 9.5 0 0 0 2.5 12a9.5 9.5 0 0 0 9.5 9.5 9.5 9.5 0 0 0 9.5-9.5A9.5 9.5 0 0 0 12 2.5zm7.962 8.75h-3.008a11.503 11.503 0 0 0-2.666-6.65h0.742a8.014 8.014 0 0 1 4.932 6.65zm-7.749 8.15h-0.429a9.988 9.988 0 0 1-3.238-6.65h6.908a9.978 9.978 0 0 1-3.241 6.65zm-3.667-8.15a10.007 10.007 0 0 1 3.232-6.65h0.446a10.015 10.015 0 0 1 3.231 6.65H8.546zM8.97 4.6h0.743a11.499 11.499 0 0 0-2.667 6.65H4.038A8.014 8.014 0 0 1 8.97 4.6zm-4.932 8.15h3.008a11.476 11.476 0 0 0 2.67 6.65H8.97a8.014 8.014 0 0 1-4.932-6.65zM15.03 19.4h-0.733a11.465 11.465 0 0 0 2.658-6.65h3.007a8.014 8.014 0 0 1-4.932 6.65z" /> diff --git a/app/src/main/res/drawable/ic_link_disabled.xml b/app/src/main/res/drawable/ic_link_disabled.xml index 083c07dc6..b0084eee8 100644 --- a/app/src/main/res/drawable/ic_link_disabled.xml +++ b/app/src/main/res/drawable/ic_link_disabled.xml @@ -7,7 +7,14 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> + + android:pathData="M10.08 15.526l4.324 4.324h1.13l3.815-3.815v-1.13l-4.324-4.324a1.5 1.5 0 0 1 2.12 0l3.123 3.123a2.498 2.498 0 0 1 0 3.533l-3.533 3.533a2.498 2.498 0 0 1-3.533 0l-3.123-3.123a1.5 1.5 0 0 1 0.001-2.121z" /> + + diff --git a/app/src/main/res/drawable/ic_link_enabled.xml b/app/src/main/res/drawable/ic_link_enabled.xml index 87e43535b..ecbc5488c 100644 --- a/app/src/main/res/drawable/ic_link_enabled.xml +++ b/app/src/main/res/drawable/ic_link_enabled.xml @@ -9,5 +9,11 @@ android:viewportHeight="24"> + android:pathData="M10.08 15.526l4.324 4.324h1.13l3.815-3.815v-1.13l-4.324-4.324a1.5 1.5 0 0 1 2.12 0l3.123 3.123a2.498 2.498 0 0 1 0 3.533l-3.533 3.533a2.498 2.498 0 0 1-3.533 0l-3.123-3.123a1.5 1.5 0 0 1 0.001-2.121z" /> + + diff --git a/app/src/main/res/drawable/ic_location.xml b/app/src/main/res/drawable/ic_location.xml index a6fbc4b6d..13454f7f2 100644 --- a/app/src/main/res/drawable/ic_location.xml +++ b/app/src/main/res/drawable/ic_location.xml @@ -10,13 +10,4 @@ - - - diff --git a/app/src/main/res/drawable/ic_location_anim_disable.xml b/app/src/main/res/drawable/ic_location_anim_disable.xml deleted file mode 100644 index 4664906a7..000000000 --- a/app/src/main/res/drawable/ic_location_anim_disable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_location_anim_enable.xml b/app/src/main/res/drawable/ic_location_anim_enable.xml deleted file mode 100644 index 89b92fd4a..000000000 --- a/app/src/main/res/drawable/ic_location_anim_enable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_location_disabled.xml b/app/src/main/res/drawable/ic_location_disabled.xml index b7f85fa0a..9b0e5fb95 100644 --- a/app/src/main/res/drawable/ic_location_disabled.xml +++ b/app/src/main/res/drawable/ic_location_disabled.xml @@ -3,25 +3,14 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + diff --git a/app/src/main/res/drawable/ic_location_enabled.xml b/app/src/main/res/drawable/ic_location_enabled.xml index 41270ef01..785b356ab 100644 --- a/app/src/main/res/drawable/ic_location_enabled.xml +++ b/app/src/main/res/drawable/ic_location_enabled.xml @@ -3,25 +3,14 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + diff --git a/app/src/main/res/drawable/ic_login.xml b/app/src/main/res/drawable/ic_login.xml index e95281678..a61455db0 100644 --- a/app/src/main/res/drawable/ic_login.xml +++ b/app/src/main/res/drawable/ic_login.xml @@ -3,11 +3,11 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_microphone.xml b/app/src/main/res/drawable/ic_microphone.xml index e3d9712f5..69a5db52e 100644 --- a/app/src/main/res/drawable/ic_microphone.xml +++ b/app/src/main/res/drawable/ic_microphone.xml @@ -11,12 +11,4 @@ android:id="@+id/disabled" android:drawable="@drawable/ic_microphone_disabled" /> - - - + diff --git a/app/src/main/res/drawable/ic_microphone_anim_disable.xml b/app/src/main/res/drawable/ic_microphone_anim_disable.xml deleted file mode 100644 index 556580d5b..000000000 --- a/app/src/main/res/drawable/ic_microphone_anim_disable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_microphone_anim_enable.xml b/app/src/main/res/drawable/ic_microphone_anim_enable.xml deleted file mode 100644 index bccb4de81..000000000 --- a/app/src/main/res/drawable/ic_microphone_anim_enable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_microphone_disabled.xml b/app/src/main/res/drawable/ic_microphone_disabled.xml index ffe797fc9..929cb80a0 100644 --- a/app/src/main/res/drawable/ic_microphone_disabled.xml +++ b/app/src/main/res/drawable/ic_microphone_disabled.xml @@ -3,25 +3,11 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_microphone_enabled.xml b/app/src/main/res/drawable/ic_microphone_enabled.xml index 148a6695d..e639d9faa 100644 --- a/app/src/main/res/drawable/ic_microphone_enabled.xml +++ b/app/src/main/res/drawable/ic_microphone_enabled.xml @@ -7,21 +7,10 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - + + diff --git a/app/src/main/res/drawable/ic_multiple_tabs.xml b/app/src/main/res/drawable/ic_multiple_tabs.xml index e57048109..4df88b72c 100644 --- a/app/src/main/res/drawable/ic_multiple_tabs.xml +++ b/app/src/main/res/drawable/ic_multiple_tabs.xml @@ -8,6 +8,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_new.xml b/app/src/main/res/drawable/ic_new.xml index 52f76c04b..ea1ea7ba1 100644 --- a/app/src/main/res/drawable/ic_new.xml +++ b/app/src/main/res/drawable/ic_new.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M19.25 12.75c0.207 0 0.395-0.084 0.53-0.22C19.916 12.394 20 12.207 20 12c0-0.414-0.336-0.75-0.75-0.75h-5.7l-0.8-0.8v-5.7C12.75 4.336 12.414 4 12 4s-0.75 0.336-0.75 0.75v5.7l-0.8 0.8h-5.7C4.336 11.25 4 11.586 4 12s0.336 0.75 0.75 0.75h5.7l0.8 0.8v5.7c0 0.414 0.336 0.75 0.75 0.75 0.207 0 0.395-0.084 0.53-0.22 0.136-0.136 0.22-0.323 0.22-0.53v-5.7l0.8-0.8h5.7z" /> diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml index 23fd8c1d0..813693ef5 100644 --- a/app/src/main/res/drawable/ic_notifications.xml +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -10,13 +10,4 @@ - - - diff --git a/app/src/main/res/drawable/ic_notifications_anim_disable.xml b/app/src/main/res/drawable/ic_notifications_anim_disable.xml deleted file mode 100644 index b54382c08..000000000 --- a/app/src/main/res/drawable/ic_notifications_anim_disable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_notifications_anim_enable.xml b/app/src/main/res/drawable/ic_notifications_anim_enable.xml deleted file mode 100644 index 8f4aa3bda..000000000 --- a/app/src/main/res/drawable/ic_notifications_anim_enable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_notifications_disabled.xml b/app/src/main/res/drawable/ic_notifications_disabled.xml index 9c30ac7bd..dcce0b521 100644 --- a/app/src/main/res/drawable/ic_notifications_disabled.xml +++ b/app/src/main/res/drawable/ic_notifications_disabled.xml @@ -7,21 +7,10 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - - + + + diff --git a/app/src/main/res/drawable/ic_notifications_enabled.xml b/app/src/main/res/drawable/ic_notifications_enabled.xml index 86447ec03..8fcc4d39d 100644 --- a/app/src/main/res/drawable/ic_notifications_enabled.xml +++ b/app/src/main/res/drawable/ic_notifications_enabled.xml @@ -7,21 +7,10 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - + + 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 index 422897fd9..c8312c4bf 100644 --- a/app/src/main/res/drawable/ic_onboarding_avatar_anonymous_large.xml +++ b/app/src/main/res/drawable/ic_onboarding_avatar_anonymous_large.xml @@ -8,9 +8,12 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:fillColor="?onboardingSelected" + android:pathData="M12 6.5a3 3 0 1 0 0 6 3 3 0 0 0 0-6z" /> + android:fillColor="?onboardingSelected" + android:pathData="M12 21.5c-5.238 0-9.5-4.262-9.5-9.5S6.762 2.5 12 2.5s9.5 4.262 9.5 9.5-4.262 9.5-9.5 9.5zM12 4c-4.411 0-8 3.589-8 8s3.589 8 8 8 8-3.589 8-8-3.589-8-8-8z" /> + diff --git a/app/src/main/res/drawable/ic_onboarding_privacy_notice.xml b/app/src/main/res/drawable/ic_onboarding_privacy_notice.xml deleted file mode 100644 index e836da928..000000000 --- a/app/src/main/res/drawable/ic_onboarding_privacy_notice.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_onboarding_tracking_protection.xml b/app/src/main/res/drawable/ic_onboarding_tracking_protection.xml index 6ec758a14..3fc0e565c 100644 --- a/app/src/main/res/drawable/ic_onboarding_tracking_protection.xml +++ b/app/src/main/res/drawable/ic_onboarding_tracking_protection.xml @@ -2,23 +2,29 @@ - + + android:pathData="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z"> - - + android:endX="3" + android:endY="22" + android:startX="21" + android:startY="2" + android:type="linear"> + + diff --git a/app/src/main/res/drawable/ic_pdd.png b/app/src/main/res/drawable/ic_pdd.png new file mode 100644 index 000000000..c96a7c578 Binary files /dev/null and b/app/src/main/res/drawable/ic_pdd.png differ diff --git a/app/src/main/res/drawable/ic_private_browsing.xml b/app/src/main/res/drawable/ic_private_browsing.xml index baf1a8062..10276e31e 100644 --- a/app/src/main/res/drawable/ic_private_browsing.xml +++ b/app/src/main/res/drawable/ic_private_browsing.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M21.914 8.476c-1.906-2.502-5.646-2.821-7.976-0.681l-1.303 1.197h-1.276l-1.303-1.197c-2.329-2.14-6.07-1.82-7.976 0.681-0.247 0.975 0.134 4.575 0.35 5.141 0.434 2.287 2.303 4.007 4.543 4.007 1.119 0 2.132-0.447 2.933-1.161l0.488-0.418c0.91-0.778 2.217-0.789 3.138-0.026l0.733 0.608v-0.003c0.772 0.621 1.718 1.001 2.754 1.001 2.24 0 4.109-1.72 4.543-4.007 0.218-0.567 0.613-4.152 0.352-5.142zM9.79 12.045C9.257 12.626 8.43 13 7.5 13c-0.93 0-1.757-0.374-2.29-0.955-0.28-0.305-0.28-0.785 0-1.09C5.743 10.374 6.57 10 7.5 10c0.93 0 1.757 0.374 2.29 0.955 0.28 0.305 0.28 0.785 0 1.09zm9 0C18.257 12.626 17.43 13 16.5 13c-0.93 0-1.757-0.374-2.29-0.955-0.28-0.305-0.28-0.785 0-1.09C14.743 10.374 15.57 10 16.5 10c0.93 0 1.757 0.374 2.29 0.955 0.28 0.305 0.28 0.785 0 1.09z" /> diff --git a/app/src/main/res/drawable/ic_qr.xml b/app/src/main/res/drawable/ic_qr.xml index aa81e7c49..2b237163d 100644 --- a/app/src/main/res/drawable/ic_qr.xml +++ b/app/src/main/res/drawable/ic_qr.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M15.8 4.5a0.8 0.8 0 0 1 0-1.5h2.7A2.5 2.5 0 0 1 21 5.5v2.8a0.8 0.8 0 0 1-1.5 0v-3l-0.8-0.8h-3zm-7.6 0a0.8 0.8 0 0 0 0-1.5H5.5A2.5 2.5 0 0 0 3 5.5v2.8a0.8 0.8 0 0 0 1.5 0v-3l0.8-0.8h3zm7.6 15a0.8 0.8 0 0 0 0 1.5h2.7a2.5 2.5 0 0 0 2.5-2.5v-2.8a0.8 0.8 0 0 0-1.5 0v3l-0.8 0.8h-3zm-7.6 0a0.8 0.8 0 0 1 0 1.5H5.5A2.5 2.5 0 0 1 3 18.5v-2.8a0.8 0.8 0 0 1 1.5 0v3l0.8 0.8h3zm2-8.5H7.8A0.8 0.8 0 0 1 7 10.2V7.8A0.8 0.8 0 0 1 7.8 7h2.5A0.8 0.8 0 0 1 11 7.8v2.5a0.8 0.8 0 0 1-0.8 0.7zm6.6 4H15v1.8s0.1 0.2 0.3 0.2h1.4a0.3 0.3 0 0 0 0.3-0.3v-1.4a0.3 0.3 0 0 0-0.3-0.3zm0-6H15V7.2A0.3 0.3 0 0 1 15.3 7h1.4A0.3 0.3 0 0 1 17 7.3v1.5A0.3 0.3 0 0 1 16.7 9zM15 11h-1.8a0.3 0.3 0 0 1-0.2-0.3V9.3A0.3 0.3 0 0 1 13.3 9H15v2zm1.8 2H15v-2h1.8a0.3 0.3 0 0 1 0.2 0.3v1.4a0.3 0.3 0 0 1-0.3 0.3zM15 13h-1.8a0.3 0.3 0 0 0-0.2 0.3V15h2v-2zm-2 2h-2v1.8s0.1 0.2 0.3 0.2h1.4a0.3 0.3 0 0 0 0.3-0.3V15zm-2.3-2H9.3A0.3 0.3 0 0 0 9 13.3V15h2v-1.8a0.3 0.3 0 0 0-0.3-0.2zM9 15H7.2A0.3 0.3 0 0 0 7 15.3v1.4C7 16.9 7.1 17 7.3 17h1.5A0.3 0.3 0 0 0 9 16.7V15z" /> diff --git a/app/src/main/res/drawable/ic_readermode.xml b/app/src/main/res/drawable/ic_readermode.xml index d5aac5b81..396e54072 100644 --- a/app/src/main/res/drawable/ic_readermode.xml +++ b/app/src/main/res/drawable/ic_readermode.xml @@ -9,5 +9,8 @@ android:viewportHeight="24"> + android:pathData="M6.5 3A2.5 2.5 0 0 0 4 5.5v13A2.5 2.5 0 0 0 6.5 21h11a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 17.5 3h-11zm12 15.7l-0.8 0.8H6.3l-0.8-0.8V5.3l0.8-0.8h11.4l0.8 0.8v13.4z" /> + diff --git a/app/src/main/res/drawable/ic_readermode_selected.xml b/app/src/main/res/drawable/ic_readermode_selected.xml index c72b5267b..9a87ee124 100644 --- a/app/src/main/res/drawable/ic_readermode_selected.xml +++ b/app/src/main/res/drawable/ic_readermode_selected.xml @@ -9,7 +9,20 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M6.5 3A2.5 2.5 0 0 0 4 5.5v13A2.5 2.5 0 0 0 6.5 21h11a2.5 2.5 0 0 0 2.5-2.5v-13A2.5 2.5 0 0 0 17.5 3h-11zm12 15.7l-0.8 0.8H6.3l-0.8-0.8V5.3l0.8-0.8h11.4l0.8 0.8v13.4z" > + + + + + + android:pathData="M15.685,14.074C16.511,12.925 17,11.519 17,10C17,6.141 13.86,3 10,3C6.14,3 3,6.141 3,10C3,13.859 6.14,17 10,17C11.525,17 12.936,16.508 14.087,15.676L14.611,15.67L19.721,20.78C19.867,20.926 20.059,21 20.251,21C20.443,21 20.635,20.927 20.781,20.78C21.074,20.487 21.074,20.012 20.781,19.719L15.675,14.612L15.685,14.074ZM10,15.5C6.967,15.5 4.5,13.032 4.5,10C4.5,6.968 6.967,4.5 10,4.5C13.033,4.5 15.5,6.968 15.5,10C15.5,13.032 13.033,15.5 10,15.5Z" /> diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index 051a9abb9..6c3315b26 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -8,6 +8,9 @@ android:viewportWidth="24" android:viewportHeight="24"> + android:pathData="M12.8,22h-1.6a2,2 0,0 1,-1.9 -1.4l-0.5,-1.8 -1,-0.6 -1.9,0.4a2,2 0,0 1,-2.2 -1L3,16.4a2,2 0,0 1,0.3 -2.4l1.3,-1.3a6.7,6.7 0,0 1,0 -1.2l-1.3,-1.3A2,2 0,0 1,3 7.7l0.8,-1.4a2,2 0,0 1,2.2 -1l1.9,0.5 1,-0.6 0.5,-1.8a2,2 0,0 1,2 -1.4h1.5a2,2 0,0 1,2 1.4l0.4,1.8 1,0.6 1.9,-0.4a2,2 0,0 1,2.2 1l0.8,1.3a2,2 0,0 1,-0.3 2.4l-1.3,1.3a6.6,6.6 0,0 1,0 1.2l1.3,1.3c0.6,0.7 0.7,1.7 0.3,2.4l-0.8,1.4a2,2 0,0 1,-2.2 1l-1.8,-0.5 -1,0.6 -0.6,1.8a2,2 0,0 1,-2 1.4zM7.8,16.6l0.6,0.2 1.3,0.7 0.4,0.5 0.6,2.1c0.1,0.3 0.3,0.4 0.5,0.4h1.6c0.2,0 0.4,-0.1 0.5,-0.4l0.6,-2 0.4,-0.6a6,6 0,0 0,1.3 -0.7l0.7,-0.2 2.1,0.6c0.2,0 0.5,0 0.6,-0.3l0.8,-1.3 -0.1,-0.6 -1.6,-1.7 -0.2,-0.6a5.8,5.8 0,0 0,0 -1.5l0.2,-0.6L19.7,9c0.1,-0.1 0.2,-0.4 0,-0.6L19,7.1a0.5,0.5 0,0 0,-0.6 -0.3l-2.2,0.6 -0.6,-0.2a6,6 0,0 0,-1.3 -0.7L14,6l-0.6,-2.1c0,-0.3 -0.3,-0.4 -0.5,-0.4h-1.6c-0.2,0 -0.4,0.1 -0.5,0.4l-0.6,2 -0.4,0.6a6,6 0,0 0,-1.3 0.7l-0.6,0.2 -2.2,-0.6c-0.2,0 -0.5,0 -0.6,0.3l-0.8,1.3 0.1,0.6L6,10.6l0.2,0.6a6.1,6.1 0,0 0,0 1.5l-0.2,0.7L4.3,15c-0.1,0.1 -0.2,0.4 0,0.6l0.7,1.3c0.1,0.2 0.4,0.3 0.6,0.3l2.1,-0.6z" + android:fillColor="?primaryText"/> + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml new file mode 100644 index 000000000..c36e1839d --- /dev/null +++ b/app/src/main/res/drawable/ic_share.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_share_filled.xml b/app/src/main/res/drawable/ic_share_filled.xml deleted file mode 100644 index d8b6ccbf8..000000000 --- a/app/src/main/res/drawable/ic_share_filled.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_sign_in.xml b/app/src/main/res/drawable/ic_sign_in.xml index e02a2ef30..28629bb9c 100644 --- a/app/src/main/res/drawable/ic_sign_in.xml +++ b/app/src/main/res/drawable/ic_sign_in.xml @@ -8,6 +8,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_signed_out.xml b/app/src/main/res/drawable/ic_signed_out.xml new file mode 100644 index 000000000..2a52d161e --- /dev/null +++ b/app/src/main/res/drawable/ic_signed_out.xml @@ -0,0 +1,20 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_social_media_trackers.xml b/app/src/main/res/drawable/ic_social_media_trackers.xml index fe473248a..a1c78bd68 100644 --- a/app/src/main/res/drawable/ic_social_media_trackers.xml +++ b/app/src/main/res/drawable/ic_social_media_trackers.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M4.5,15h-1A1.5,1.5 0,0 1,2 13.5v-8A1.5,1.5 0,0 1,3.5 4h1A1.5,1.5 0,0 1,6 5.5v8A1.5,1.5 0,0 1,4.5 15zM18.767,16.077a2.461,2.461 0,0 0,2.335 -3.24l-2.051,-6.154A2.46,2.46 0,0 0,16.716 5H8v9.023l2.154,3.285 1.037,3.112a0.85,0.85 0,0 0,0.806 0.58H12c1.02,0 1.846,-0.827 1.846,-1.846v-2.708l0.369,-0.369h4.552z" /> diff --git a/app/src/main/res/drawable/ic_static_shortcut_private_tab_foreground.xml b/app/src/main/res/drawable/ic_static_shortcut_private_tab_foreground.xml index 77047eb00..0b22e26b8 100644 --- a/app/src/main/res/drawable/ic_static_shortcut_private_tab_foreground.xml +++ b/app/src/main/res/drawable/ic_static_shortcut_private_tab_foreground.xml @@ -9,7 +9,7 @@ - + android:pathData="M7.2,11h-0.9l-0.3,0.3v0.9l0.3,0.3h0.9l0.3,-0.3v-0.9l-0.3,-0.3z" + android:fillColor="@color/disabled_text"/> + + diff --git a/app/src/main/res/drawable/ic_storage_enabled.xml b/app/src/main/res/drawable/ic_storage_enabled.xml index dc941e619..b5d2f7b4a 100644 --- a/app/src/main/res/drawable/ic_storage_enabled.xml +++ b/app/src/main/res/drawable/ic_storage_enabled.xml @@ -11,5 +11,8 @@ android:autoMirrored="true"> - + android:pathData="M7.2 11H6.3L6 11.3v0.9l0.3 0.3h0.9l0.3-0.3v-0.9L7.2 11z" /> + + diff --git a/app/src/main/res/drawable/ic_sync_disconnected.xml b/app/src/main/res/drawable/ic_sync_disconnected.xml index ab6bd0299..b4d61e51c 100644 --- a/app/src/main/res/drawable/ic_sync_disconnected.xml +++ b/app/src/main/res/drawable/ic_sync_disconnected.xml @@ -3,13 +3,14 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + android:fillColor="?primaryText" + android:pathData="M10.07 20.336l1.198-2.089A6.364 6.364 0 0 1 9.5 18.5c-2.353 0-4.395-1.277-5.525-3.162a2.451 2.451 0 0 1 1.84-0.838h7.372c0.075 0 0.144 0.017 0.217 0.024l1.28-2.231c0.916-1.596 3.218-1.596 4.134 0l0.12 0.208a9.71 9.71 0 0 0-0.07-2.613c-0.65-3.933-3.823-7.105-7.756-7.755C4.62 1.06-0.94 6.62 0.133 13.111c0.65 3.933 3.822 7.105 7.755 7.755a9.794 9.794 0 0 0 1.93 0.111c0.052-0.217 0.132-0.432 0.251-0.641zM9.5 6.5a3 3 0 1 1 0 6 3 3 0 0 1 0-6z" /> + diff --git a/app/src/main/res/drawable/ic_synced_tabs.xml b/app/src/main/res/drawable/ic_synced_tabs.xml index a707e9daf..5968047c9 100644 --- a/app/src/main/res/drawable/ic_synced_tabs.xml +++ b/app/src/main/res/drawable/ic_synced_tabs.xml @@ -8,9 +8,9 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_tab_collection.xml b/app/src/main/res/drawable/ic_tab_collection.xml index b78a92c35..df421740c 100644 --- a/app/src/main/res/drawable/ic_tab_collection.xml +++ b/app/src/main/res/drawable/ic_tab_collection.xml @@ -9,8 +9,8 @@ android:viewportHeight="24"> + android:pathData="M8.75 12.5h6.5a0.75 0.75 0 0 0 0-1.5h-6.5a0.75 0.75 0 0 0 0 1.5zm0 4h6.5a0.75 0.75 0 0 0 0-1.5h-6.5a0.75 0.75 0 0 0 0 1.5z" /> - + android:pathData="M19.962 7.811l0.018-0.005-0.788-2.951A2.504 2.504 0 0 0 16.776 3H7.22a2.502 2.502 0 0 0-2.419 1.868L4.088 7.596C4.072 7.632 4.07 7.673 4.058 7.711L4.041 7.777l0.004 0.001C4.028 7.852 4 7.921 4 8v10.5A2.5 2.5 0 0 0 6.5 21h11a2.5 2.5 0 0 0 2.5-2.5V8c0-0.067-0.026-0.125-0.038-0.189zM7.12 4.5h9.756l0.858 0.713L18.212 7H5.794l0.484-1.787L7.12 4.5zM18.5 18.7l-0.8 0.8H6.3l-0.8-0.8V8.5h13v10.2z" /> + diff --git a/app/src/main/res/drawable/ic_tabs.xml b/app/src/main/res/drawable/ic_tabs.xml index eed851286..04f47f556 100644 --- a/app/src/main/res/drawable/ic_tabs.xml +++ b/app/src/main/res/drawable/ic_tabs.xml @@ -8,7 +8,7 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_top_sites.xml b/app/src/main/res/drawable/ic_top_sites.xml index 44ca20fe3..af3d6c0b4 100644 --- a/app/src/main/res/drawable/ic_top_sites.xml +++ b/app/src/main/res/drawable/ic_top_sites.xml @@ -9,6 +9,6 @@ android:viewportHeight="24"> + android:pathData="M21.28 20.22l-5.472-5.472 2.986-2.986c0.527-0.528 0.68-1.285 0.398-1.976a1.796 1.796 0 0 0-1.665-1.133l-2.475-0.029-3.775-3.776 1.068-1.068a0.75 0.75 0 1 0-1.061-1.061L2.72 11.287a0.75 0.75 0 0 0 1.06 1.061l1.079-1.079 3.775 3.775 0.029 2.475a1.794 1.794 0 0 0 1.135 1.665 1.795 1.795 0 0 0 1.973-0.398l2.977-2.977 5.472 5.472a0.748 0.748 0 0 0 1.06 0 0.75 0.75 0 0 0 0-1.061zm-10.569-2.495a0.304 0.304 0 0 1-0.348 0.07 0.307 0.307 0 0 1-0.2-0.294l-0.037-3.086-4.207-4.207 4.299-4.299 4.206 4.207 3.086 0.037a0.306 0.306 0 0 1 0.294 0.199 0.31 0.31 0 0 1-0.07 0.349l-7.023 7.024z" /> diff --git a/app/src/main/res/drawable/ic_tracking_protection.xml b/app/src/main/res/drawable/ic_tracking_protection.xml index ed41ca68e..64f2d3cac 100644 --- a/app/src/main/res/drawable/ic_tracking_protection.xml +++ b/app/src/main/res/drawable/ic_tracking_protection.xml @@ -11,12 +11,4 @@ android:id="@+id/disabled" android:drawable="@drawable/ic_tracking_protection_disabled" /> - - diff --git a/app/src/main/res/drawable/ic_tracking_protection_anim_disable.xml b/app/src/main/res/drawable/ic_tracking_protection_anim_disable.xml deleted file mode 100644 index 14f17035d..000000000 --- a/app/src/main/res/drawable/ic_tracking_protection_anim_disable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_tracking_protection_anim_enable.xml b/app/src/main/res/drawable/ic_tracking_protection_anim_enable.xml deleted file mode 100644 index 3080ef51b..000000000 --- a/app/src/main/res/drawable/ic_tracking_protection_anim_enable.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_tracking_protection_disabled.xml b/app/src/main/res/drawable/ic_tracking_protection_disabled.xml index 0ef9a1b5c..54d9215ad 100644 --- a/app/src/main/res/drawable/ic_tracking_protection_disabled.xml +++ b/app/src/main/res/drawable/ic_tracking_protection_disabled.xml @@ -7,21 +7,10 @@ android:height="24dp" android:viewportWidth="24" android:viewportHeight="24"> - - - - - - - - + + diff --git a/app/src/main/res/drawable/ic_tracking_protection_enabled.xml b/app/src/main/res/drawable/ic_tracking_protection_enabled.xml index 633271b15..2c5cf04a7 100644 --- a/app/src/main/res/drawable/ic_tracking_protection_enabled.xml +++ b/app/src/main/res/drawable/ic_tracking_protection_enabled.xml @@ -3,25 +3,11 @@ - 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:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + diff --git a/app/src/main/res/drawable/ic_whats_new.xml b/app/src/main/res/drawable/ic_whats_new.xml index 7fb9e238c..c96c8b5b9 100644 --- a/app/src/main/res/drawable/ic_whats_new.xml +++ b/app/src/main/res/drawable/ic_whats_new.xml @@ -9,5 +9,5 @@ android:viewportHeight="24"> + android:pathData="M18.5 7h-1.1A3.3 3.3 0 0 0 17 3a3.3 3.3 0 0 0-4.6 0 2 2 0 0 0-0.3 0.2A1.9 1.9 0 0 0 12 3a3.3 3.3 0 0 0-4.6 0A3.3 3.3 0 0 0 7 7H5.5A2.5 2.5 0 0 0 3 9.5v1c0 0.8 0.4 1.5 1 2v6A2.5 2.5 0 0 0 6.5 21h11a2.5 2.5 0 0 0 2.5-2.5v-6a2.5 2.5 0 0 0 1-2v-1A2.5 2.5 0 0 0 18.5 7zm0.2 1.5l0.8 0.8v1.4l-0.8 0.8h-6v-3h6zM13.4 4c0.7-0.7 1.8-0.7 2.5 0a1.8 1.8 0 0 1 0 2.5C15.5 6.9 14 7 12.9 7V6.3c0-1 0.2-2 0.5-2.3zm-5 2.5a1.8 1.8 0 0 1 1.2-3c0.5 0 1 0.2 1.3 0.5 0.3 0.3 0.5 1.3 0.5 2.3V7h-0.8c-1 0-1.9-0.2-2.2-0.5zM4.5 9.3l0.8-0.8h6v3h-6l-0.8-0.8V9.3zm1.8 10.2l-0.8-0.8V13h5.8v6.5h-5zm12.2-0.8l-0.8 0.8h-5V13h5.8v5.7z"/> diff --git a/app/src/main/res/drawable/mozac_ic_extensions_black.xml b/app/src/main/res/drawable/mozac_ic_extensions_black.xml index 097a4a094..9395d6040 100644 --- a/app/src/main/res/drawable/mozac_ic_extensions_black.xml +++ b/app/src/main/res/drawable/mozac_ic_extensions_black.xml @@ -5,9 +5,9 @@ + android:viewportWidth="24" + android:viewportHeight="24"> + android:pathData="M17.5,7H15.5V5.127C15.5,3.533 14.317,2.166 12.807,2.015C11.958,1.931 11.116,2.206 10.488,2.775C9.86,3.342 9.5,4.154 9.5,4.999V7H6.5C5.119,7 4,8.119 4,9.5V11.75C4,12.44 4.56,13 5.25,13H6.873C7.706,13 8.417,13.59 8.493,14.343C8.536,14.775 8.402,15.188 8.114,15.506C7.829,15.82 7.424,16 7.001,16H5.25C4.56,16 4,16.56 4,17.25V19.5C4,20.881 5.119,22 6.5,22H17.5C18.881,22 20,20.881 20,19.5V9.5C20,8.119 18.881,7 17.5,7ZM18.5,19.7L17.7,20.5H6.3L5.5,19.7V17.5H7.001C7.847,17.5 8.658,17.14 9.226,16.512C9.794,15.885 10.07,15.039 9.985,14.194C9.834,12.683 8.467,11.5 6.873,11.5H5.5V9.3L6.3,8.5H10C10.552,8.5 11,8.052 11,7.5V4.999C11,4.576 11.18,4.171 11.494,3.887C11.812,3.599 12.227,3.466 12.657,3.507C13.41,3.582 14,4.294 14,5.127V7.5C14,8.052 14.448,8.5 15,8.5H17.7L18.5,9.3V19.7Z" /> diff --git a/app/src/main/res/drawable/private_browsing_button.xml b/app/src/main/res/drawable/private_browsing_button.xml index 344500088..be68036ec 100644 --- a/app/src/main/res/drawable/private_browsing_button.xml +++ b/app/src/main/res/drawable/private_browsing_button.xml @@ -23,7 +23,7 @@ android:viewportHeight="24"> + android:pathData="M21.914 8.476c-1.906-2.502-5.646-2.821-7.976-0.681l-1.303 1.197h-1.276l-1.303-1.197c-2.329-2.14-6.07-1.82-7.976 0.681-0.247 0.975 0.134 4.575 0.35 5.141 0.434 2.287 2.303 4.007 4.543 4.007 1.119 0 2.132-0.447 2.933-1.161l0.488-0.418c0.91-0.778 2.217-0.789 3.138-0.026l0.733 0.608v-0.003c0.772 0.621 1.718 1.001 2.754 1.001 2.24 0 4.109-1.72 4.543-4.007 0.218-0.567 0.613-4.152 0.352-5.142zM9.79 12.045C9.257 12.626 8.43 13 7.5 13c-0.93 0-1.757-0.374-2.29-0.955-0.28-0.305-0.28-0.785 0-1.09C5.743 10.374 6.57 10 7.5 10c0.93 0 1.757 0.374 2.29 0.955 0.28 0.305 0.28 0.785 0 1.09zm9 0C18.257 12.626 17.43 13 16.5 13c-0.93 0-1.757-0.374-2.29-0.955-0.28-0.305-0.28-0.785 0-1.09C14.743 10.374 15.57 10 16.5 10c0.93 0 1.757 0.374 2.29 0.955 0.28 0.305 0.28 0.785 0 1.09z" /> diff --git a/app/src/main/res/drawable/shield_dark.xml b/app/src/main/res/drawable/shield_dark.xml index a230ffa1e..f97e24bdb 100644 --- a/app/src/main/res/drawable/shield_dark.xml +++ b/app/src/main/res/drawable/shield_dark.xml @@ -13,9 +13,8 @@ android:viewportHeight="24"> + android:fillColor="#FF00DDFF" + android:pathData="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" /> @@ -25,7 +24,7 @@ android:interpolator="@android:interpolator/fast_out_slow_in" android:propertyName="fillColor" android:valueFrom="#fbfbfe" - android:valueTo="#00b3f4" + android:valueTo="#FF00DDFF" android:valueType="colorType" /> diff --git a/app/src/main/res/drawable/shield_light.xml b/app/src/main/res/drawable/shield_light.xml index 083f74145..f621f8390 100644 --- a/app/src/main/res/drawable/shield_light.xml +++ b/app/src/main/res/drawable/shield_light.xml @@ -14,8 +14,7 @@ + android:pathData="M12.0002,22C11.5342,22 11.0662,21.885 10.6462,21.668C8.4962,20.547 6.5242,18.735 5.0942,16.566C4.3852,15.493 3.8882,14.198 3.6162,12.716L2.9092,8.872C2.6692,7.563 3.2942,6.259 4.4632,5.625L10.5652,2.363C11.4632,1.88 12.5362,1.88 13.4322,2.359L19.5342,5.682C20.7082,6.313 21.3352,7.621 21.0912,8.936L20.3862,12.728C20.1152,14.201 19.6172,15.49 18.9112,16.56C17.4822,18.729 15.5102,20.542 13.3562,21.666C12.9372,21.885 12.4682,22 12.0002,22ZM11.5152,3.556L4.8452,7.13L4.3382,8.368L5.0922,12.445C5.3272,13.727 5.7502,14.836 6.3472,15.74C7.6392,17.7 9.4122,19.332 11.3392,20.337H12.6632C14.5932,19.329 16.3672,17.694 17.6582,15.735C18.2532,14.833 18.6752,13.729 18.9102,12.455L19.6632,8.368L19.0532,7.129L12.4942,3.557H11.5152V3.556Z" /> @@ -25,7 +24,7 @@ android:interpolator="@android:interpolator/fast_out_slow_in" android:propertyName="fillColor" android:valueFrom="#20123a" - android:valueTo="#0250bb" + android:valueTo="#FF0250BB" android:valueType="colorType" /> diff --git a/app/src/main/res/drawable/top_sites_background.xml b/app/src/main/res/drawable/top_sites_background.xml new file mode 100644 index 000000000..c5ff9918d --- /dev/null +++ b/app/src/main/res/drawable/top_sites_background.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/layout/activity_home.xml b/app/src/main/res/layout/activity_home.xml index 5dc4bdefa..c016bdfd3 100644 --- a/app/src/main/res/layout/activity_home.xml +++ b/app/src/main/res/layout/activity_home.xml @@ -17,11 +17,11 @@ android:layout_width="match_parent" android:layout_height="56dp" /> + + app:defaultNavHost="true"/> diff --git a/app/src/main/res/layout/checkbox_left_sub_preference.xml b/app/src/main/res/layout/checkbox_left_sub_preference.xml index 796d6be42..2913a9811 100644 --- a/app/src/main/res/layout/checkbox_left_sub_preference.xml +++ b/app/src/main/res/layout/checkbox_left_sub_preference.xml @@ -7,6 +7,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:minHeight="48dp" android:background="?android:selectableItemBackground" android:clickable="true" android:focusable="true" diff --git a/app/src/main/res/layout/collection_home_list_row.xml b/app/src/main/res/layout/collection_home_list_row.xml index 15feff9e8..d37675e47 100644 --- a/app/src/main/res/layout/collection_home_list_row.xml +++ b/app/src/main/res/layout/collection_home_list_row.xml @@ -48,8 +48,8 @@ tools:text="@tools:sample/lorem/random" /> + + + + + + + + diff --git a/app/src/main/res/layout/component_sync_tabs_tray_layout.xml b/app/src/main/res/layout/component_sync_tabs_tray_layout.xml new file mode 100644 index 000000000..451b9a780 --- /dev/null +++ b/app/src/main/res/layout/component_sync_tabs_tray_layout.xml @@ -0,0 +1,29 @@ + + + + + + + + diff --git a/app/src/main/res/layout/component_tabstray2.xml b/app/src/main/res/layout/component_tabstray2.xml index 6bcbe4e4f..81fab8e3b 100644 --- a/app/src/main/res/layout/component_tabstray2.xml +++ b/app/src/main/res/layout/component_tabstray2.xml @@ -25,30 +25,13 @@ app:layout_constraintWidth_percent="0.1" /> - - + android:layout="@layout/tabs_tray_tab_counter2" /> + + + app:layout_constraintTop_toBottomOf="@+id/info_banner" /> + + + + + + + diff --git a/app/src/main/res/layout/default_browser_experiment_preference.xml b/app/src/main/res/layout/default_browser_experiment_preference.xml new file mode 100644 index 000000000..028e8d017 --- /dev/null +++ b/app/src/main/res/layout/default_browser_experiment_preference.xml @@ -0,0 +1,47 @@ + + + + + + + + +