diff --git a/.github/ISSUE_TEMPLATE/---bug-report.md b/.github/ISSUE_TEMPLATE/---bug-report.md index b9d0ec5da..e27d73793 100644 --- a/.github/ISSUE_TEMPLATE/---bug-report.md +++ b/.github/ISSUE_TEMPLATE/---bug-report.md @@ -18,6 +18,6 @@ assignees: '' ### Device information -* Android device: ? -* Fenix version: ? +* Device vendor / model and Android version: ? +* Firefox for Android version: ? (go to Settings -> About Firefox) diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..64e7e858f --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,62 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 180 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 7 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pin + - "feature request 🌟" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: wontfix + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + See: https://github.com/mozilla-mobile/fenix/issues/17373 + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. + +# Comment to post when removing the stale label. +# unmarkComment: > +# Your comment here. + +# Comment to post when closing a stale Issue or Pull Request. +# closeComment: > +# Your comment here. + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Limit to only `issues` or `pulls` +only: issues + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +# pulls: +# daysUntilStale: 30 +# markComment: > +# This pull request has been automatically marked as stale because it has not had +# recent activity. It will be closed if no further activity occurs. Thank you +# for your contributions. + +issues: + exemptLabels: + - pin + - "feature request 🌟" diff --git a/.github/workflows/build-contributor-pr.yml b/.github/workflows/build-contributor-pr.yml new file mode 100644 index 000000000..b966cb796 --- /dev/null +++ b/.github/workflows/build-contributor-pr.yml @@ -0,0 +1,103 @@ +name: Android build PR +on: [pull_request] +jobs: + run-build: + runs-on: ubuntu-20.04 + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: "Clean & Assemble Debug" + uses: eskatos/gradle-command-action@v1 + with: + wrapper-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + arguments: clean app:assembleDebug + + run-testDebugUnitTest: + runs-on: ubuntu-20.04 + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: "Test Debug Unit Tests" + uses: eskatos/gradle-command-action@v1 + with: + wrapper-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + arguments: testDebugUnitTest + + run-detekt: + runs-on: ubuntu-20.04 + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: "Detekt" + uses: eskatos/gradle-command-action@v1 + with: + wrapper-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + arguments: detekt + - name: Archive detekt results + uses: actions/upload-artifact@v2 + with: + name: detekt report + path: build/reports/detekt.html + + run-ktlint: + runs-on: ubuntu-20.04 + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: "Ktlint" + uses: eskatos/gradle-command-action@v1 + with: + wrapper-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + arguments: ktlint + + run-lintDebug: + runs-on: ubuntu-20.04 + if: github.event.pull_request.head.repo.full_name != github.repository + steps: + - name: Checkout repository + uses: actions/checkout@v2 + - name: Setup Java + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: "Lint Debug" + uses: eskatos/gradle-command-action@v1 + with: + wrapper-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + arguments: lintDebug + - name: Archive lint results + uses: actions/upload-artifact@v2 + with: + name: lintDebug report + path: app/build/reports/lint-results-debug.html + diff --git a/.mergify.yml b/.mergify.yml index 1028efadc..21382057c 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -21,7 +21,7 @@ pull_request_rules: conditions: - author=mozilla-l10n-automation-bot - status-success=pr-complete - - files~=(strings.xml) + - files~=(strings.xml|l10n.toml) actions: review: type: APPROVE diff --git a/.taskcluster.yml b/.taskcluster.yml index 4ee1f0b8c..2e71a5c7b 100644 --- a/.taskcluster.yml +++ b/.taskcluster.yml @@ -103,7 +103,7 @@ tasks: $if: > tasks_for in ["action", "cron"] || (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"]) - || (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") + || (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") && (head_branch[:8] != "mergify/") || (tasks_for == "github-release" && releaseAction == "published" && (ownerEmail != "mozilla-release-automation-bot@users.noreply.github.com") && (ownerEmail != "mozilla-release-automation-bot-staging@users.noreply.github.com")) then: $let: @@ -166,7 +166,7 @@ tasks: routes: $flattenDeep: - checks - - $if: 'level == "3"' + - $if: 'level == "3" || repoUrl == "https://github.com/mozilla-releng/staging-fenix"' then: - tc-treeherder.v2.${project}.${head_sha} # TODO Bug 1601928: Make this scope fork-friendly once ${project} is better defined. This will enable diff --git a/app/build.gradle b/app/build.gradle index dc1be5113..13507c0b3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,12 +41,25 @@ android { buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\"" buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\"" buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\"" + // These add-ons should be excluded for Mozilla Online builds. + buildConfigField "String[]", "MOZILLA_ONLINE_ADDON_EXCLUSIONS", + "{" + + "\"uBlock0@raymondhill.net\"," + + "\"firefox@ghostery.com\"," + + "\"jid1-MnnxcxisBPnSXQ@jetpack\"," + + "\"adguardadblocker@adguard.com\"," + + "\"foxyproxy@eric.h.jung\"," + + "\"{73a6fe31-595d-460b-a920-fcc0f8843232}\"," + + "\"jid1-BoFifL9Vbdl2zQ@jetpack\"," + + "\"woop-NoopscooPsnSXQ@jetpack\"" + + "}" // This should be the base URL used to call the AMO API. buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\"" def deepLinkSchemeValue = "fenix-dev" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" manifestPlaceholders = [ - "deepLinkScheme": deepLinkSchemeValue + "deepLinkScheme": deepLinkSchemeValue, + "requestLegacyExternalStorage": true ] // Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`. @@ -81,13 +94,19 @@ android { applicationIdSuffix ".fenix.debug" resValue "bool", "IS_DEBUG", "true" pseudoLocalesEnabled true + def deepLinkSchemeValue = "fenix-dev" + buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" + manifestPlaceholders = [ + "deepLinkScheme": deepLinkSchemeValue, + "requestLegacyExternalStorage": false + ] } nightly releaseTemplate >> { applicationIdSuffix ".fenix" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" def deepLinkSchemeValue = "fenix-nightly" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" - manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue] + manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue, "requestLegacyExternalStorage": false] } beta releaseTemplate >> { buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" @@ -103,7 +122,8 @@ android { // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", - "deepLinkScheme": deepLinkSchemeValue + "deepLinkScheme": deepLinkSchemeValue, + "requestLegacyExternalStorage": true ] } release releaseTemplate >> { @@ -120,7 +140,8 @@ android { // - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36905922 "sharedUserId": "org.mozilla.firefox.sharedID", - "deepLinkScheme": deepLinkSchemeValue + "deepLinkScheme": deepLinkSchemeValue, + "requestLegacyExternalStorage": true ] } forkDebug { @@ -398,6 +419,16 @@ android.applicationVariants.all { variant -> buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' println("--") } + +// ------------------------------------------------------------------------------------------------- +// BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central. +// ------------------------------------------------------------------------------------------------- + + if (project.hasProperty("official") || gradle.hasProperty("localProperties.official")) { + buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'true' + } else { + buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false' + } } androidExtensions { @@ -507,6 +538,7 @@ dependencies { implementation Deps.mozilla_feature_top_sites implementation Deps.mozilla_feature_share implementation Deps.mozilla_feature_accounts_push + implementation Deps.mozilla_feature_webauthn implementation Deps.mozilla_feature_webcompat implementation Deps.mozilla_feature_webnotifications implementation Deps.mozilla_feature_webcompat_reporter diff --git a/app/metrics.yaml b/app/metrics.yaml index 5291a91ca..d2c763aca 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -234,6 +234,25 @@ events: notification_emails: - fenix-core@mozilla.com expires: "2021-08-01" + normal_and_private_uri_count: + type: counter + description: | + A counter of URIs visited by the user in the current session, including + page reloads. This includes private browsing. This does not include + background page requests and URIs from embedded pages but may be + incremented without user interaction by website scripts that + programmatically redirect to a new location. + send_in_pings: + - metrics + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17089 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17935 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2022-08-01" preference_toggled: type: event description: | @@ -623,7 +642,7 @@ login_dialog: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" cancelled: type: event description: | @@ -636,7 +655,7 @@ login_dialog: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" saved: type: event description: | @@ -649,7 +668,7 @@ login_dialog: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" never_save: type: event description: | @@ -662,7 +681,7 @@ login_dialog: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" find_in_page: opened: @@ -3859,14 +3878,21 @@ addons: expires: "2021-04-01" startup.timeline: - framework_start: + framework_primary: send_in_pings: - startup-timeline type: timespan - time_unit: nanosecond + time_unit: millisecond description: | The duration the Android framework takes to start before letting us run - code in `*Application.init`. This is calculated from `appInitTimestamp - + code in `*Application.init` when this device has `clock_ticks_per_second` + equal to 100: if it's not equal to 100, then this value is captured in + `framework_secondary`. We split this into two metrics to make it easier + to analyze in GLAM. We split on 100 because when we did our initial brief + analysis - https://sql.telemetry.mozilla.org/queries/75591 - the results + for clocks ticks were overwhelmingly 100. + + The duration is calculated from `appInitTimestamp - processStartTimestamp`. `processStartTimestamp` is derived from the clock tick time unit, which is expected to be less granular than nanoseconds. Therefore, we convert and round our timestamps to clock ticks before @@ -3876,9 +3902,30 @@ startup.timeline: devices, is also reported as a metric bugs: - https://github.com/mozilla-mobile/fenix/issues/8803 + - https://github.com/mozilla-mobile/fenix/issues/17972 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 + - https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-01" + framework_secondary: + send_in_pings: + - startup-timeline + type: timespan + time_unit: millisecond + description: | + The duration the Android framework takes to start before letting us run + code in `*Application.init` when this device has `clock_ticks_per_second` + not equal to 100. For more details on this metric, see `framework_primary` + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17972 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284 data_sensitivity: - technical notification_emails: @@ -3890,9 +3937,9 @@ startup.timeline: - startup-timeline type: boolean description: | - An error when attempting to record `framework_start` - the application - init timestamp returned a negative value - which is likely indicative of a - bug in the implementation. + An error when attempting to record `framework_primary/secondary` - the + application init timestamp returned a negative value - which is likely + indicative of a bug in the implementation. bugs: - https://github.com/mozilla-mobile/fenix/issues/8803 data_reviews: @@ -3943,6 +3990,38 @@ startup.timeline: - mcomella@mozilla.com expires: "2021-08-01" +perf.startup: + application_on_create: + type: timing_distribution + time_unit: millisecond + description: | + The duration of `FenixApplication.onCreate` in the main process. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17969 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" + home_activity_on_create: + type: timing_distribution + time_unit: millisecond + description: | + The duration of `HomeActivity.onCreate`. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17969 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889 + data_sensitivity: + - technical + notification_emails: + - perf-android-fe@mozilla.com + - mcomella@mozilla.com + expires: "2021-08-11" + perf.awesomebar: history_suggestions: send_in_pings: @@ -4083,7 +4162,7 @@ autoplay: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" setting_changed: type: event description: | @@ -4102,7 +4181,7 @@ autoplay: - interaction notification_emails: - fenix-core@mozilla.com - expires: "2021-02-01" + expires: "2021-08-01" storage.stats: query_stats_duration: @@ -4308,6 +4387,47 @@ tabs: - fenix-core@mozilla.com expires: "2021-08-01" +banner_open_in_app: + displayed: + type: event + description: | + Open in App banner was shown. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/16828 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17049 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-08-01" + dismissed: + type: event + description: | + User tapped 'dismiss' on Open in App banner. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/16828 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17049 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-08-01" + go_to_settings: + type: event + description: | + User tapped 'go to settings' on Open in App banner. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/16828 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17049 + data_sensitivity: + - interaction + notification_emails: + - fenix-core@mozilla.com + expires: "2021-08-01" + contextual_menu: copy_tapped: type: event @@ -4361,3 +4481,156 @@ contextual_menu: notification_emails: - fenix-core@mozilla.com expires: "2021-06-01" + +engine: + tab_kills: + type: labeled_counter + labels: + - foreground + - background + description: | + How often was the content process of a foreground (selected) or + background tab killed. + bugs: + - https://github.com/mozilla-mobile/android-components/issues/9366 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17864 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + - skaspari@mozilla.com + expires: "2021-12-31" + kill_foreground_age: + type: timespan + time_unit: millisecond + description: | + Measures the age of the engine session of a foreground (selected) tab + at the time its content process got killed. + bugs: + - https://github.com/mozilla-mobile/android-components/issues/9366 + data_reviews: + - TBD + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + - skaspari@mozilla.com + expires: "2021-12-31" + kill_background_age: + type: timespan + time_unit: millisecond + description: | + Measures the age of the engine session of a background tab at the + time its content process got killed. + bugs: + - https://github.com/mozilla-mobile/android-components/issues/9366 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/17864 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + - skaspari@mozilla.com + expires: "2021-12-31" + +android_keystore_experiment: + experiment_failure: + type: event + description: | + Records an instance of an unexpected failure during the experiment + extra_keys: + failure_exception: + description: | + Exception class associated with an unexpected failure of this + experiment, not caught by the other failure handlers. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" + get_failure: + type: event + description: | + Unexpected failure when trying to read from secure prefs. + extra_keys: + failure_exception: + description: | + Exception class associated with an unexpected failure of this + experiment. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" + get_result: + type: event + description: | + Success when trying to read from secure prefs. + extra_keys: + result: + description: | + Result code identifying whether the read operation returned the + expected value or not. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" + write_failure: + type: event + description: | + Unexpected failure when trying to write to secure prefs. + extra_keys: + failure_exception: + description: | + Exception class associated with an unexpected failure of this + experiment. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" + write_success: + type: event + description: | + Success in writing to secure prefs. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" + reset: + type: event + description: | + An experiment failed, and was reset to run again in the future from a + blank state. + bugs: + - https://github.com/mozilla-mobile/fenix/issues/17869 + data_reviews: + - https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395 + data_sensitivity: + - technical + notification_emails: + - fenix-core@mozilla.com + expires: "2021-09-01" diff --git a/app/pings.yaml b/app/pings.yaml index ae1474c71..bd726ff12 100644 --- a/app/pings.yaml +++ b/app/pings.yaml @@ -12,8 +12,8 @@ activation: an hashed version of the Google Advertising ID. include_client_id: false bugs: - - 1538011 - - 1501822 + - https://bugzilla.mozilla.com/1538011/ + - https://bugzilla.mozilla.com/1501822/ data_reviews: - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 notification_emails: @@ -35,14 +35,19 @@ startup-timeline: description: | This ping is intended to provide an understanding of startup performance. - The ping is intended to be captured by performance testing automation to - report results there, in addition to user telemetry. We place these metrics - into their own ping in order to isolate them and make this process easier. - include_client_id: false + In addition to being captured on real devices, the ping data was prematurely + optimized into this separate ping to be isolated from other metrics to be + more easily captured by performance testing automation but that hasn't + happened in practice. We would have removed it but implementation + details don't make that possible: + https://github.com/mozilla-mobile/fenix/issues/17972#issuecomment-781002987 + include_client_id: true bugs: - https://github.com/mozilla-mobile/fenix/issues/8803 + - https://github.com/mozilla-mobile/fenix/issues/17972 data_reviews: - https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626 + - https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284 notification_emails: - perf-android-fe@mozilla.com - - esmyth@mozilla.com + - mcomella@mozilla.com diff --git a/app/src/androidTest/assets/pages/trackingPage.html b/app/src/androidTest/assets/pages/trackingPage.html index 1a0d735a6..a0a53c954 100644 --- a/app/src/androidTest/assets/pages/trackingPage.html +++ b/app/src/androidTest/assets/pages/trackingPage.html @@ -9,5 +9,71 @@ + +

Level 1 (Basic) List

+

social-track-digest256:

+ +
+

ads-track-digest256:

+ +
+

analytics-track-digest256:

+ +
+

Fingerprinting: +

test not run
+ +

+
+

Cryptomining: + not blocked +

+ +

Cookie blocking +

+ + + +

+ * Facebook-cookies


+    * LinkedIn-cookies 

+    * Twitter-cookies 

+    

+ + diff --git a/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt index afcc8d50e..c3a26ae21 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/glean/BaselinePingTest.kt @@ -152,7 +152,7 @@ class BaselinePingTest { .click() // Validate the received data. - val baselinePing = waitForPingContent("baseline", "background")!! + val baselinePing = waitForPingContent("baseline", "inactive")!! val metrics = baselinePing.getJSONObject("metrics") diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt index 10b8b5d33..a3f049647 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/HomeActivityTestRule.kt @@ -4,6 +4,7 @@ package org.mozilla.fenix.helpers +import android.view.ViewConfiguration.getLongPressTimeout import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.ActivityTestRule @@ -25,11 +26,18 @@ class HomeActivityTestRule( private val skipOnboarding: Boolean = false ) : ActivityTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) { + private val longTapUserPreference = getLongPressTimeout() + override fun beforeActivityLaunched() { super.beforeActivityLaunched() - setLongTapTimeout() + setLongTapTimeout(3000) if (skipOnboarding) { skipOnboardingBeforeLaunch() } } + + override fun afterActivityFinished() { + super.afterActivityFinished() + setLongTapTimeout(longTapUserPreference) + } } /** @@ -46,17 +54,24 @@ class HomeActivityIntentTestRule( private val skipOnboarding: Boolean = false ) : IntentsTestRule(HomeActivity::class.java, initialTouchMode, launchActivity) { + private val longTapUserPreference = getLongPressTimeout() + override fun beforeActivityLaunched() { super.beforeActivityLaunched() - setLongTapTimeout() + setLongTapTimeout(3000) if (skipOnboarding) { skipOnboardingBeforeLaunch() } } + + override fun afterActivityFinished() { + super.afterActivityFinished() + setLongTapTimeout(longTapUserPreference) + } } // changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click -fun setLongTapTimeout() { +fun setLongTapTimeout(delay: Int) { val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) - mDevice.executeShellCommand("settings put secure long_press_timeout 3000") + mDevice.executeShellCommand("settings put secure long_press_timeout $delay") } private fun skipOnboardingBeforeLaunch() { diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt index a32fe743d..0d3537f65 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/SessionLoadedIdlingResource.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.helpers import androidx.test.core.app.ApplicationProvider import androidx.test.espresso.IdlingResource +import mozilla.components.browser.state.selector.selectedTab import org.mozilla.fenix.FenixApplication /** @@ -21,13 +22,12 @@ class SessionLoadedIdlingResource : IdlingResource { override fun isIdleNow(): Boolean { val context = ApplicationProvider.getApplicationContext() - val sessionManager = context.components.core.sessionManager - val session = sessionManager.selectedSession + val selectedTab = context.components.core.store.state.selectedTab - return if (session?.loading == true) { + return if (selectedTab?.content?.loading == true) { false } else { - if (session?.progress == 100) { + if (selectedTab?.content?.progress == 100) { invokeCallback() true } else { diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt index ad339c592..9dffa1728 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt @@ -12,11 +12,6 @@ import android.os.Build import android.os.Environment import androidx.preference.PreferenceManager import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.ViewAction -import androidx.test.espresso.action.CoordinatesProvider -import androidx.test.espresso.action.GeneralClickAction -import androidx.test.espresso.action.Press -import androidx.test.espresso.action.Tap import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.matcher.ViewMatchers.withId @@ -108,22 +103,6 @@ object TestHelper { } } - fun sendSingleTapToScreen(x: Int, y: Int): ViewAction? { - return GeneralClickAction( - Tap.SINGLE, - CoordinatesProvider { view -> - val screenPos = IntArray(2) - view.getLocationOnScreen(screenPos) - val screenX = screenPos[0] + x.toFloat() - val screenY = screenPos[1] + y.toFloat() - floatArrayOf(screenX, screenY) - }, - Press.FINGER, - 0, - 0 - ) - } - // Remove test file from the device Downloads folder @Suppress("Deprecation") fun deleteDownloadFromStorage(fileName: String) { diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt index 601c450cf..e818a0733 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/ext/ViewInteraction.kt @@ -4,7 +4,14 @@ package org.mozilla.fenix.helpers +import android.view.InputDevice +import android.view.MotionEvent +import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.GeneralClickAction +import androidx.test.espresso.action.GeneralLocation +import androidx.test.espresso.action.Press +import androidx.test.espresso.action.Tap import androidx.test.espresso.action.ViewActions import androidx.test.espresso.assertion.ViewAssertions.matches @@ -21,3 +28,21 @@ fun ViewInteraction.assertIsChecked(isChecked: Boolean): ViewInteraction { fun ViewInteraction.assertIsSelected(isSelected: Boolean): ViewInteraction { return this.check(matches(isSelected(isSelected)))!! } + +/** + * Perform a click (simulate the finger touching the View) at a specific location in the View + * rather than the default middle of the View. + * + * Useful in situations where the View we want clicked contains other Views in it's x,y middle + * and we need to simulate the touch in some other free space of the View we want clicked. + */ +fun ViewInteraction.clickAtLocationInView(locationInView: GeneralLocation): ViewAction = + ViewActions.actionWithAssertions( + GeneralClickAction( + Tap.SINGLE, + locationInView, + Press.FINGER, + InputDevice.SOURCE_UNKNOWN, + MotionEvent.BUTTON_PRIMARY + ) + ) 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 0fa99cc35..d0f32270d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/screenshots/DefaultHomeScreenTest.kt @@ -44,10 +44,14 @@ class DefaultHomeScreenTest : ScreenshotTest() { SystemClock.sleep(TestAssetHelper.waitingTimeShort) Screengrab.screenshot("HomeScreenRobot_home-screen") }.openThreeDotMenu { - }.openSettings { } + }.openSettings { + }.openPrivateBrowsingSubMenu { + clickPrivateModeScreenshotsSwitch() + } // To get private screenshot, // dismiss onboarding going to settings and back mDevice.pressBack() + mDevice.pressBack() homeScreen { togglePrivateBrowsingModeOnOff() Screengrab.screenshot("HomeScreenRobot_private-browsing-menu") 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 dcf5a6c4f..3b50a489f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -494,12 +494,16 @@ class BookmarksTest { }.openBookmarks { createFolder("1") getInstrumentation().waitForIdleSync() + waitForBookmarksFolderContentToExist("Bookmarks", "1") selectFolder("1") + verifyCurrentFolderTitle("1") createFolder("2") getInstrumentation().waitForIdleSync() + waitForBookmarksFolderContentToExist("1", "2") selectFolder("2") verifyCurrentFolderTitle("2") navigateUp() + waitForBookmarksFolderContentToExist("1", "2") verifyCurrentFolderTitle("1") mDevice.pressBack() verifyBookmarksMenuView() 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 c877171ec..258a2a803 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ContextMenusTest.kt @@ -69,6 +69,7 @@ class ContextMenusTest { snackBarButtonClick("Switch") verifyUrl(genericURL.url.toString()) }.openTabDrawer { + verifyNormalModeSelected() verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_4") } 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 b29837983..ee42a73e6 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -66,7 +66,6 @@ class NavigationToolbarTest { } } - @Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12894") @Test fun goForwardTest() { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) @@ -79,7 +78,8 @@ class NavigationToolbarTest { }.enterURLAndEnterToBrowser(nextWebPage.url) { mDevice.waitForIdle() verifyUrl(nextWebPage.url.toString()) - mDevice.pressBack() + }.openThreeDotMenu { + }.goBack { mDevice.waitForIdle() verifyUrl(defaultWebPage.url.toString()) } 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 d7e0c50b8..3d37cf44e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt @@ -12,11 +12,14 @@ import org.junit.Rule import org.junit.Test import org.mozilla.fenix.ui.robots.navigationToolbar import androidx.test.espresso.IdlingRegistry +import org.junit.Ignore +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.mDevice /** @@ -28,10 +31,10 @@ import org.mozilla.fenix.ui.robots.mDevice * */ -// @Ignore("Temp disable - reader view page detection issues: https://github.com/mozilla-mobile/fenix/issues/9688 ") class ReaderViewTest { private lateinit var mockWebServer: MockWebServer private var readerViewNotification: ViewVisibilityIdlingResource? = null + private val estimatedReadingTime = "1 - 2 minutes" @get:Rule val activityIntentTestRule = HomeActivityIntentTestRule() @@ -101,6 +104,7 @@ class ReaderViewTest { @Test fun verifyReaderViewToggle() { + // New three-dot menu design does not have readerview appearance menu item val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -119,18 +123,27 @@ class ReaderViewTest { navigationToolbar { verifyReaderViewDetected(true) toggleReaderView() - }.openThreeDotMenu { - verifyReaderViewAppearance(true) - }.closeBrowserMenuToBrowser { } + mDevice.waitForIdle() + } + + if (!FeatureFlags.toolbarMenuFeature) { + browserScreen { + verifyPageContent(estimatedReadingTime) + }.openThreeDotMenu { + verifyReaderViewAppearance(true) + }.closeBrowserMenuToBrowser { } + } navigationToolbar { toggleReaderView() + mDevice.waitForIdle() }.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) @@ -150,6 +163,11 @@ class ReaderViewTest { navigationToolbar { verifyReaderViewDetected(true) toggleReaderView() + mDevice.waitForIdle() + } + + browserScreen { + verifyPageContent(estimatedReadingTime) }.openThreeDotMenu { verifyReaderViewAppearance(true) }.openReaderViewAppearance { @@ -166,6 +184,7 @@ class ReaderViewTest { } @Test + @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") fun verifyReaderViewAppearanceFontSizeToggle() { val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -185,6 +204,11 @@ class ReaderViewTest { navigationToolbar { verifyReaderViewDetected(true) toggleReaderView() + mDevice.waitForIdle() + } + + browserScreen { + verifyPageContent(estimatedReadingTime) }.openThreeDotMenu { verifyReaderViewAppearance(true) }.openReaderViewAppearance { @@ -207,6 +231,7 @@ class ReaderViewTest { } @Test + @Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971") fun verifyReaderViewAppearanceColorSchemeChange() { val readerViewPage = TestAssetHelper.getLoremIpsumAsset(mockWebServer) @@ -226,6 +251,11 @@ class ReaderViewTest { navigationToolbar { verifyReaderViewDetected(true) toggleReaderView() + mDevice.waitForIdle() + } + + browserScreen { + verifyPageContent(estimatedReadingTime) }.openThreeDotMenu { verifyReaderViewAppearance(true) }.openReaderViewAppearance { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt index 79057f88c..9bd09b80b 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsAddonsTest.kt @@ -79,19 +79,23 @@ class SettingsAddonsTest { val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val addonName = "uBlock Origin" - navigationToolbar { - }.openNewTabAndEnterToBrowser(defaultWebPage.url) { - }.openThreeDotMenu { - }.openAddonsManagerMenu { - addonsListIdlingResource = - RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1) - IdlingRegistry.getInstance().register(addonsListIdlingResource!!) - clickInstallAddon(addonName) - verifyAddonPrompt(addonName) - cancelInstallAddon() - clickInstallAddon(addonName) - acceptInstallAddon() - verifyDownloadAddonPrompt(addonName, activityTestRule) + navigationToolbar {} + .openNewTabAndEnterToBrowser(defaultWebPage.url) {} + .openThreeDotMenu {} + .openAddonsManagerMenu { + addonsListIdlingResource = + RecyclerViewIdlingResource( + activityTestRule.activity.findViewById(R.id.add_ons_list), + 1 + ) + IdlingRegistry.getInstance().register(addonsListIdlingResource!!) + clickInstallAddon(addonName) + verifyAddonPrompt(addonName) + cancelInstallAddon() + clickInstallAddon(addonName) + acceptInstallAddon() + + verifyDownloadAddonPrompt(addonName, activityTestRule) } } 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 28efbb031..6cc2b33d8 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -27,7 +27,9 @@ import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.ui.robots.browserScreen +import org.mozilla.fenix.ui.robots.clickTabCrashedRestoreButton import org.mozilla.fenix.ui.robots.clickUrlbar +import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.enhancedTrackingProtection import org.mozilla.fenix.ui.robots.homeScreen @@ -48,10 +50,6 @@ class SmokeTest { private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null private var readerViewNotification: ViewVisibilityIdlingResource? = null private val downloadFileName = "Globe.svg" - private val searchEngine = object { - var title = "Ecosia" - var url = "https://www.ecosia.org/search?q=%s" - } val collectionName = "First Collection" private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null @@ -201,6 +199,7 @@ 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) @@ -365,6 +364,7 @@ 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) @@ -372,7 +372,7 @@ class SmokeTest { }.openThreeDotMenu { }.openSettings { }.openEnhancedTrackingProtectionSubMenu { - clickEnhancedTrackingProtectionDefaults() + switchEnhancedTrackingProtectionToggle() verifyEnhancedTrackingProtectionOptionsGrayedOut() }.goBackToHomeScreen { navigationToolbar { @@ -381,7 +381,7 @@ class SmokeTest { }.openThreeDotMenu { }.openSettings { }.openEnhancedTrackingProtectionSubMenu { - clickEnhancedTrackingProtectionDefaults() + switchEnhancedTrackingProtectionToggle() }.goBack { }.goBackToBrowser { clickEnhancedTrackingProtectionPanel() @@ -391,6 +391,32 @@ class SmokeTest { } } + @Test + fun customTrackingProtectionSettingsTest() { + val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) + + homeScreen { + }.openThreeDotMenu { + }.openSettings { + }.openEnhancedTrackingProtectionSubMenu { + verifyEnhancedTrackingProtectionOptions() + selectTrackingProtectionOption("Custom") + verifyCustomTrackingProtectionSettings() + }.goBackToHomeScreen {} + + navigationToolbar { + }.enterURLAndEnterToBrowser(trackingPage.url) {} + + enhancedTrackingProtection { + dismissTrackingOnboarding() + }.openEnhancedTrackingProtectionSheet { + verifyTrackingCookiesBlocked() + verifyCryptominersBlocked() + verifyFingerprintersBlocked() + verifyBasicLevelTrackingContentBlocked() + } + } + @Test // Verifies changing the default engine from the Search Shortcut menu fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() { @@ -495,31 +521,6 @@ class SmokeTest { } @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/17847") - @Test - // Verifies setting as default a customized search engine name and URL - fun editCustomSearchEngineTest() { - homeScreen { - }.openThreeDotMenu { - }.openSettings { - }.openSearchSubMenu { - openAddSearchEngineMenu() - selectAddCustomSearchEngine() - typeCustomEngineDetails(searchEngine.title, searchEngine.url) - saveNewSearchEngine() - openEngineOverflowMenu("Ecosia") - clickEdit() - typeCustomEngineDetails("Test", searchEngine.url) - saveEditSearchEngine() - changeDefaultSearchEngine("Test") - }.goBack { - }.goBack { - }.openSearch { - verifyDefaultSearchEngine("Test") - clickSearchEngineShortcutButton() - verifyEnginesListShortcutContains("Test") - } - } - @Test // Swipes the nav bar left/right to switch between tabs fun swipeToSwitchTabTest() { @@ -532,14 +533,15 @@ class SmokeTest { }.openNewTab { }.submitQuery(secondWebPage.url.toString()) { swipeNavBarRight(secondWebPage.url.toString()) - verifyPageContent(firstWebPage.content) + verifyUrl(firstWebPage.url.toString()) swipeNavBarLeft(firstWebPage.url.toString()) - verifyPageContent(secondWebPage.content) + verifyUrl(secondWebPage.url.toString()) } } @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) @@ -603,6 +605,7 @@ 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 @@ -1110,6 +1113,7 @@ 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/" @@ -1126,10 +1130,12 @@ 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 = TestAssetHelper.getLoremIpsumAsset(mockWebServer) + val estimatedReadingTime = "1 - 2 minutes" navigationToolbar { }.enterURLAndEnterToBrowser(readerViewPage.url) { @@ -1146,6 +1152,11 @@ class SmokeTest { navigationToolbar { verifyReaderViewDetected(true) toggleReaderView() + mDevice.waitForIdle() + } + + browserScreen { + verifyPageContent(estimatedReadingTime) }.openThreeDotMenu { verifyReaderViewAppearance(true) }.openReaderViewAppearance { @@ -1160,4 +1171,31 @@ class SmokeTest { verifyAppearanceColorSepia(true) } } + + @Test + fun closeTabCrashedReporterTest() { + + homeScreen { + }.openNavigationToolbar { + }.openTabCrashReporter { + }.clickTabCrashedCloseButton { + }.openTabDrawer { + verifyNoTabsOpened() + } + } + + @Test + fun restoreTabCrashedReporterTest() { + val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) + + homeScreen { + }.openNavigationToolbar { + }.enterURLAndEnterToBrowser(website.url) {} + + navigationToolbar { + }.openTabCrashReporter { + clickTabCrashedRestoreButton() + verifyPageContent(website.content) + } + } } 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 659f7547e..c3583b70d 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/StrictEnhancedTrackingProtectionTest.kt @@ -8,6 +8,7 @@ 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 @@ -70,7 +71,7 @@ class StrictEnhancedTrackingProtectionTest { }.openEnhancedTrackingProtectionSubMenu { verifyEnhancedTrackingProtectionHeader() verifyEnhancedTrackingProtectionOptions() - verifyEnhancedTrackingProtectionDefaults() + verifyTrackingProtectionSwitchEnabled() }.openExceptions { verifyDefault() } @@ -126,6 +127,7 @@ 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) @@ -177,7 +179,7 @@ class StrictEnhancedTrackingProtectionTest { }.openProtectionSettings { verifyEnhancedTrackingProtectionHeader() verifyEnhancedTrackingProtectionOptions() - verifyEnhancedTrackingProtectionDefaults() + verifyTrackingProtectionSwitchEnabled() } settingsSubMenuEnhancedTrackingProtection { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt index 24d78cc61..73a3a4977 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/TabbedBrowsingTest.kt @@ -10,13 +10,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior 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.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.TestAssetHelper -import org.mozilla.fenix.helpers.TestHelper.sendSingleTapToScreen import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.navigationToolbar @@ -55,16 +53,6 @@ class TabbedBrowsingTest { } } - // 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() @@ -72,8 +60,6 @@ class TabbedBrowsingTest { @Test fun openNewTabTest() { - homeScreen { }.dismissOnboarding() - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { @@ -81,26 +67,26 @@ class TabbedBrowsingTest { mDevice.waitForIdle() verifyTabCounter("1") }.openTabDrawer { - verifyExistingTabList() - }.openTabsListThreeDotMenu { - verifyCloseAllTabsButton() - verifyShareTabButton() - verifySelectTabs() + verifyNormalModeSelected() + verifyExistingOpenTabs("Test_Page_1") + closeTab() + }.openTabDrawer { + verifyNoTabsOpened() + }.openNewTab { + }.submitQuery(defaultWebPage.url.toString()) { + mDevice.waitForIdle() + verifyTabCounter("1") + }.openTabDrawer { + verifyNormalModeSelected() + verifyExistingOpenTabs("Test_Page_1") } } @Test fun openNewPrivateTabTest() { - homeScreen { }.dismissOnboarding() - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) - homeScreen { }.togglePrivateBrowsingMode() - - homeScreen { - verifyPrivateSessionMessage() - verifyTabButton() - } + homeScreen {}.togglePrivateBrowsingMode() navigationToolbar { }.openNewTabAndEnterToBrowser(defaultWebPage.url) { @@ -108,7 +94,7 @@ class TabbedBrowsingTest { verifyTabCounter("1") }.openTabDrawer { verifyExistingTabList() - verifyCloseTabsButton("Test_Page_1") + verifyPrivateModeSelected() }.toggleToNormalTabs { verifyNoTabsOpened() }.toggleToPrivateTabs { @@ -156,7 +142,6 @@ class TabbedBrowsingTest { }.openNewTabAndEnterToBrowser(genericURL.url) { }.openTabDrawer { verifyExistingOpenTabs("Test_Page_1") - verifyCloseTabsButton("Test_Page_1") closeTabViaXButton("Test_Page_1") verifySnackBarText("Tab closed") snackBarButtonClick("UNDO") @@ -187,8 +172,7 @@ class TabbedBrowsingTest { browserScreen { }.openTabDrawer { verifyExistingOpenTabs("Test_Page_1") - }.openNewTab { - }.dismissSearchBar { } + }.closeTabDrawer { } } @Test @@ -249,15 +233,12 @@ class TabbedBrowsingTest { notificationShade { verifyPrivateTabsNotification() }.clickClosePrivateTabsNotification { - // Tap an empty spot on the app homescreen to make sure it's into focus - sendSingleTapToScreen(20, 20) verifyHomeScreen() } } @Test fun verifyTabTrayNotShowingStateHalfExpanded() { - homeScreen { }.dismissOnboarding() navigationToolbar { }.openTabTray { @@ -282,8 +263,6 @@ class TabbedBrowsingTest { @Test fun verifyEmptyTabTray() { - homeScreen { }.dismissOnboarding() - navigationToolbar { }.openTabTray { verifyNoTabsOpened() @@ -298,8 +277,6 @@ class TabbedBrowsingTest { @Test fun verifyOpenTabDetails() { - homeScreen { }.dismissOnboarding() - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { @@ -317,8 +294,6 @@ class TabbedBrowsingTest { @Test fun verifyContextMenuShortcuts() { - homeScreen { }.dismissOnboarding() - val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) navigationToolbar { 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 ca48ce5b6..86645af12 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ThreeDotMenuMainTest.kt @@ -12,6 +12,7 @@ import org.junit.Before import org.junit.BeforeClass import org.junit.Rule import org.junit.Test +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.ui.robots.homeScreen @@ -54,40 +55,68 @@ class ThreeDotMenuMainTest { @Test fun threeDotMenuItemsTest() { - homeScreen { - }.openThreeDotMenu { - verifySettingsButton() - verifyBookmarksButton() - verifyHistoryButton() - verifyHelpButton() - verifyWhatsNewButton() - }.openSettings { - verifySettingsView() - }.goBack { - }.openThreeDotMenu { - }.openHelp { - verifyHelpUrl() - }.openTabDrawer { - }.openNewTab { - }.dismissSearchBar { - }.openThreeDotMenu { - }.openWhatsNew { - verifyWhatsNewURL() - }.openTabDrawer { - }.openNewTab { - }.dismissSearchBar { } - - homeScreen { - }.openThreeDotMenu { - }.openBookmarks { - verifyBookmarksMenuView() - }.closeMenu { - } + 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 { - }.openHistory { - verifyHistoryMenuView() + homeScreen { + }.openThreeDotMenu { + }.openWhatsNew { + verifyWhatsNewURL() + }.openTabDrawer { + closeTab() + } + + homeScreen { + }.openThreeDotMenu { + }.openBookmarks { + verifyBookmarksMenuView() + }.closeMenu { + } + + homeScreen { + }.openThreeDotMenu { + }.openHistory { + verifyHistoryMenuView() + } } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt index 8eddc0384..3f15b840e 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt @@ -105,6 +105,10 @@ class BookmarksRobot { fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Can’t edit default folders") fun verifyCurrentFolderTitle(title: String) { + mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar") + .textContains(title)) + .waitForExists(waitingTime) + onView( allOf( withText(title), @@ -114,6 +118,14 @@ class BookmarksRobot { .check(matches(isDisplayed())) } + fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) { + mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar") + .textContains(parentFolderName)) + .waitForExists(waitingTime) + + mDevice.waitNotNull(Until.findObject(By.text(childFolderName)), waitingTime) + } + fun verifySignInToSyncButton() = signInToSyncButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) 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 efce62eec..2e7343f36 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 @@ -32,6 +32,7 @@ import androidx.test.uiautomator.By.text import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until +import mozilla.components.browser.state.selector.selectedTab import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.containsString import org.hamcrest.Matchers.not @@ -49,8 +50,8 @@ class BrowserRobot { private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource fun verifyCurrentPrivateSession(context: Context) { - val session = context.components.core.sessionManager.selectedSession - assertTrue("Current session is private", session?.private!!) + val selectedTab = context.components.core.store.state.selectedTab + assertTrue("Current session is private", selectedTab?.content?.private ?: false) } fun verifyUrl(url: String) { @@ -465,6 +466,20 @@ class BrowserRobot { HomeScreenRobot().interact() return HomeScreenRobot.Transition() } + + fun clickTabCrashedCloseButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { + + assertTrue( + mDevice.findObject(UiSelector().resourceId("$packageName:id/closeTabButton")) + .waitForExists(waitingTime) + ) + + val tabCrashedCloseButton = mDevice.findObject(text("Close tab")) + tabCrashedCloseButton.click() + + HomeScreenRobot().interact() + return HomeScreenRobot.Transition() + } } } @@ -525,3 +540,14 @@ private fun mediaPlayerPlayButton() = .className("android.widget.Button") .text("Play") ) + +fun clickTabCrashedRestoreButton() { + + assertTrue( + mDevice.findObject(UiSelector().resourceId("$packageName:id/restoreTabButton")) + .waitForExists(waitingTime) + ) + + val tabCrashRestoreButton = mDevice.findObject(UiSelector().resourceIdMatches("$packageName:id/restoreTabButton")) + tabCrashRestoreButton.click() +} diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt index aa6adc18a..5fb906ad7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/EnhancedTrackingProtectionRobot.kt @@ -8,13 +8,20 @@ package org.mozilla.fenix.ui.robots import androidx.test.espresso.Espresso.onView import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until +import org.hamcrest.Matchers.containsString import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper +import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.ext.waitNotNull @@ -37,6 +44,14 @@ class EnhancedTrackingProtectionRobot { fun verifyEnhancedTrackingProtectionDetailsStatus(status: String) = assertEnhancedTrackingProtectionDetailsStatus(status) + fun verifyTrackingCookiesBlocked() = assertTrackingCookiesBlocked() + + fun verifyFingerprintersBlocked() = assertFingerprintersBlocked() + + fun verifyCryptominersBlocked() = assertCryptominersBlocked() + + fun verifyBasicLevelTrackingContentBlocked() = assertBasicLevelTrackingContentBlocked() + class Transition { fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition { openEnhancedTrackingProtectionSheet().click() @@ -129,3 +144,45 @@ private fun openEnhancedTrackingProtectionSettings() = private fun openEnhancedTrackingProtectionDetails() = onView(ViewMatchers.withId(R.id.tracking_content)) + +private fun assertTrackingCookiesBlocked() { + mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking")) + .waitForExists(waitingTime) + onView(withId(R.id.blocking_header)).check(matches(isDisplayed())) + onView(withId(R.id.cross_site_tracking)).check(matches(isDisplayed())) +} + +private fun assertFingerprintersBlocked() { + mDevice.findObject(UiSelector().resourceId("$packageName:id/fingerprinters")) + .waitForExists(waitingTime) + onView(withId(R.id.blocking_header)).check(matches(isDisplayed())) + onView(withId(R.id.fingerprinters)).check(matches(isDisplayed())) +} + +private fun assertCryptominersBlocked() { + mDevice.findObject(UiSelector().resourceId("$packageName:id/cryptominers")) + .waitForExists(waitingTime) + onView(withId(R.id.blocking_header)).check(matches(isDisplayed())) + onView(withId(R.id.cryptominers)).check(matches(isDisplayed())) +} + +private fun assertBasicLevelTrackingContentBlocked() { + mDevice.findObject(UiSelector().resourceId("$packageName:id/tracking_content")) + .waitForExists(waitingTime) + + onView(withId(R.id.tracking_content)) + .check(matches(isDisplayed())) + .click() + onView(withId(R.id.blocking_text_list)) + .check( + matches( + withText( + containsString( + "social-track-digest256.dummytracker.org\n" + + "ads-track-digest256.dummytracker.org\n" + + "analytics-track-digest256.dummytracker.org" + ) + ) + ) + ) +} 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 7c156b5b2..0d29f036e 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 @@ -309,6 +309,7 @@ class HomeScreenRobot { } fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { + mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/menuButton")), waitingTime) threeDotButton().perform(click()) ThreeDotMenuMainRobot().interact() @@ -485,8 +486,11 @@ private fun assertFocusedNavigationToolbar() = onView(allOf(withHint("Search or enter address"))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) -private fun assertHomeScreen() = onView(ViewMatchers.withResourceName("homeLayout")) +private fun assertHomeScreen() { + mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime) + onView(ViewMatchers.withResourceName("homeLayout")) .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) +} private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton")) .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 3c63c7eb9..1a0ad0075 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 @@ -30,6 +30,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.By import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.Until import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.anyOf @@ -121,6 +122,30 @@ class NavigationToolbarRobot { return BrowserRobot.Transition() } + fun openTabCrashReporter(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { + val crashUrl = "about:crashcontent" + + sessionLoadedIdlingResource = SessionLoadedIdlingResource() + + mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")), + waitingTime + ) + urlBar().click() + mDevice.waitNotNull( + Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")), + waitingTime + ) + + awesomeBar().perform(replaceText(crashUrl), pressImeActionButton()) + + runWithIdleRes(sessionLoadedIdlingResource) { + mDevice.findObject(UiSelector().resourceId("$packageName:id/crash_tab_image")) + } + + BrowserRobot().interact() + return BrowserRobot.Transition() + } + fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_menu")), waitingTime) threeDotButton().click() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt index aea828072..2c7ffe6ac 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NotificationRobot.kt @@ -67,7 +67,7 @@ class NotificationRobot { fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { NotificationRobot().verifySystemNotificationExists("Close private tabs") - closePrivateTabsNotification().clickAndWaitForNewWindow() + closePrivateTabsNotification().click() HomeScreenRobot().interact() return HomeScreenRobot.Transition() diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt index 5f565b257..6bf120a16 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SearchRobot.kt @@ -270,6 +270,9 @@ private fun assertSearchEngineList() { } private fun assertEngineListShortcutContains(searchEngineName: String) { + mDevice.findObject(UiSelector().resourceId("$packageName:id/awesome_bar")) + .waitForExists(waitingTime) + onView(withId(R.id.awesome_bar)) .perform(swipeDown()) .check(matches(hasDescendant(withText(searchEngineName)))) 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 340699e58..14e7b0a8e 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 @@ -13,6 +13,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.Visibility +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility @@ -25,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.not +import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.assertIsChecked import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.isChecked @@ -49,9 +51,9 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot { fun verifyEnhancedTrackingProtectionOptionsGrayedOut() = assertEnhancedTrackingProtectionOptionsGrayedOut() - fun verifyEnhancedTrackingProtectionDefaults() = assertEnhancedTrackingProtectionDefaults() + fun verifyTrackingProtectionSwitchEnabled() = assertTrackingProtectionSwitchEnabled() - fun clickEnhancedTrackingProtectionDefaults() = onView(withResourceName("switch_widget")).click() + fun switchEnhancedTrackingProtectionToggle() = onView(withResourceName("switch_widget")).click() fun verifyRadioButtonDefaults() = assertRadioButtonDefaults() @@ -60,11 +62,15 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot { verifyEnhancedTrackingProtectionHeaderDescription() verifyLearnMoreText() verifyEnhancedTrackingProtectionTextWithSwitchWidget() - verifyEnhancedTrackingProtectionDefaults() + verifyTrackingProtectionSwitchEnabled() verifyRadioButtonDefaults() verifyEnhancedTrackingProtectionOptions() } + fun verifyCustomTrackingProtectionSettings() = assertCustomTrackingProtectionSettings() + + fun selectTrackingProtectionOption(option: String) = onView(withText(option)).click() + class Transition { val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!! @@ -183,7 +189,7 @@ private fun assertEnhancedTrackingProtectionOptionsGrayedOut() { .check(matches(not(isEnabled(true)))) } -private fun assertEnhancedTrackingProtectionDefaults() { +private fun assertTrackingProtectionSwitchEnabled() { onView(withResourceName("switch_widget")).check( matches( isChecked( @@ -218,3 +224,28 @@ private fun goBackButton() = private fun openExceptions() = onView(allOf(withText("Exceptions"))) + +private fun assertCustomTrackingProtectionSettings() { + scrollToElementByText("Redirect Trackers") + cookiesCheckbox().check(matches(isDisplayed())) + cookiesDropDownMenuDefault().check(matches(isDisplayed())) + trackingContentCheckbox().check(matches(isDisplayed())) + trackingcontentDropDownDefault().check(matches(isDisplayed())) + cryptominersCheckbox().check(matches(isDisplayed())) + fingerprintersCheckbox().check(matches(isDisplayed())) + redirectTrackersCheckbox().check(matches(isDisplayed())) +} + +private fun cookiesCheckbox() = onView(withText("Cookies")) + +private fun cookiesDropDownMenuDefault() = onView(withText("All cookies (will cause websites to break)")) + +private fun trackingContentCheckbox() = onView(withText("Tracking content")) + +private fun trackingcontentDropDownDefault() = onView(withText("In all tabs")) + +private fun cryptominersCheckbox() = onView(withText("Cryptominers")) + +private fun fingerprintersCheckbox() = onView(withText("Fingerprinters")) + +private fun redirectTrackersCheckbox() = onView(withText("Redirect Trackers")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt index 5ce0944af..c8ef29dd0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuPrivateBrowsingRobot.kt @@ -46,6 +46,8 @@ class SettingsSubMenuPrivateBrowsingRobot { fun verifyPrivateBrowsingShortcutIcon() = assertPrivateBrowsingShortcutIcon() + fun clickPrivateModeScreenshotsSwitch() = screenshotsInPrivateModeSwitch().click() + fun clickOpenLinksInPrivateTabSwitch() = openLinksInPrivateTabSwitch().click() fun addPrivateShortcutToHomescreen() { @@ -92,6 +94,9 @@ private fun assertNavigationToolBarHeader() { private fun openLinksInPrivateTabSwitch() = onView(withText("Open links in a private tab")) +private fun screenshotsInPrivateModeSwitch() = + onView(withText("Allow screenshots in private browsing")) + private fun addPrivateBrowsingShortcutButton() = onView(withText("Add private browsing shortcut")) private fun goBackButton() = onView(withContentDescription("Navigate up")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt index da518f563..92c29ce00 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt @@ -8,25 +8,20 @@ package org.mozilla.fenix.ui.robots import androidx.recyclerview.widget.RecyclerView import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.click -import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.isDisplayed -import androidx.test.espresso.matcher.ViewMatchers.withChild +import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withText -import androidx.test.espresso.matcher.ViewMatchers.withContentDescription -import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiSelector import org.hamcrest.CoreMatchers -import org.hamcrest.CoreMatchers.allOf import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.click @@ -67,33 +62,6 @@ class SettingsSubMenuSearchRobot { saveNewSearchEngine() } - fun selectAddCustomSearchEngine() = onView(withText("Other")).click() - - fun typeCustomEngineDetails(engineName: String, engineURL: String) { - onView(withId(R.id.edit_engine_name)) - .perform(clearText()) - .perform(typeText(engineName)) - onView(withId(R.id.edit_search_string)) - .perform(clearText()) - .perform(typeText(engineURL)) - } - - fun openEngineOverflowMenu(searchEngineName: String) { - mDevice.findObject( - UiSelector().resourceId("org.mozilla.fenix.debug:id/overflow_menu") - ).waitForExists(waitingTime) - threeDotMenu(searchEngineName).click() - } - - fun clickEdit() = onView(withText("Edit")).click() - - fun saveEditSearchEngine() { - onView(withId(R.id.save_button)).click() - mDevice.findObject( - UiSelector().resourceId("org.mozilla.fenix.debug:id/recycler_view") - ).waitForExists(waitingTime) - } - class Transition { val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) @@ -220,11 +188,3 @@ private fun addSearchEngineSaveButton() = onView(withId(R.id.add_search_engine)) private fun assertEngineListContains(searchEngineName: String) { onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName)))) } - -private fun threeDotMenu(searchEngineName: String) = - onView( - allOf( - withId(R.id.overflow_menu), - withParent(withChild(withText(searchEngineName))) - ) - ) 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 16dddca95..0f84a9ef0 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 @@ -15,6 +15,7 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.UiController import androidx.test.espresso.ViewAction +import androidx.test.espresso.action.GeneralLocation import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.replaceText @@ -42,7 +43,9 @@ import org.hamcrest.Matcher import org.mozilla.fenix.R import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime +import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.click +import org.mozilla.fenix.helpers.clickAtLocationInView import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher @@ -85,23 +88,46 @@ class TabDrawerRobot { mDevice.findObject( UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close") ).waitForExists(waitingTime) - closeTabButton().click() + + var retries = 0 // number of retries before failing, will stop at 2 + do { + closeTabButton().click() + retries++ + } while (mDevice.findObject( + UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close") + ).exists() && retries < 3 + ) } - fun swipeTabRight(title: String) = - tab(title).perform(ViewActions.swipeRight()) + fun swipeTabRight(title: String) { + var retries = 0 // number of retries before failing, will stop at 2 + while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) { + tab(title).perform(ViewActions.swipeRight()) + retries++ + } + } - fun swipeTabLeft(title: String) = - tab(title).perform(ViewActions.swipeLeft()) + fun swipeTabLeft(title: String) { + var retries = 0 // number of retries before failing, will stop at 2 + while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) { + tab(title).perform(ViewActions.swipeLeft()) + retries++ + } + } fun closeTabViaXButton(title: String) { - val closeButton = onView( - allOf( - withId(R.id.mozac_browser_tabstray_close), - withContentDescription("Close tab $title") + mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime) + var retries = 0 // number of retries before failing, will stop at 2 + do { + val closeButton = onView( + allOf( + withId(R.id.mozac_browser_tabstray_close), + withContentDescription("Close tab $title") + ) ) - ) - closeButton.perform(click()) + closeButton.perform(click()) + retries++ + } while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) } fun verifySnackBarText(expectedText: String) { @@ -231,7 +257,9 @@ class TabDrawerRobot { } fun clickTopBar(interact: TabDrawerRobot.() -> Unit): Transition { - onView(withId(R.id.topBar)).click() + // The topBar contains other views. + // Don't do the default click in the middle, rather click in some free space - top right. + onView(withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT) TabDrawerRobot().interact() return Transition() } @@ -256,8 +284,11 @@ class TabDrawerRobot { } fun waitForTabTrayBehaviorToIdle(interact: TabDrawerRobot.() -> Unit): Transition { + // Need to get the behavior of tab_wrapper and wait for that to idle. var behavior: BottomSheetBehavior<*>? = null - onView(withId(R.id.tab_wrapper)).perform(object : ViewAction { + + // Null check here since it's possible that the view is already animated away from the screen. + onView(withId(R.id.tab_wrapper))?.perform(object : ViewAction { override fun getDescription(): String { return "Postpone actions to after the BottomSheetBehavior has settled" } @@ -270,9 +301,13 @@ class TabDrawerRobot { behavior = BottomSheetBehavior.from(view!!) } }) - runWithIdleRes(BottomSheetBehaviorStateIdlingResource(behavior!!)) { - TabDrawerRobot().interact() + + behavior?.let { + runWithIdleRes(BottomSheetBehaviorStateIdlingResource(it)) { + TabDrawerRobot().interact() + } } + return Transition() } @@ -326,6 +361,11 @@ private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow)) private fun assertExistingOpenTabs(title: String) { try { + mDevice.findObject(UiSelector() + .resourceId("$packageName:id/mozac_browser_tabstray_title") + .textContains(title)) + .waitForExists(waitingTime) + tab(title).check(matches(isDisplayed())) } catch (e: NoMatchingViewException) { onView(withId(R.id.tabsTray)).perform( 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 da61cb380..cc2304f33 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 @@ -38,6 +38,7 @@ import androidx.test.uiautomator.Until import org.hamcrest.Matcher import org.hamcrest.Matchers.allOf 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 @@ -124,21 +125,33 @@ class ThreeDotMenuMainRobot { fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyThreeDotMainMenuItems() { - verifyAddOnsButton() - verifyDownloadsButton() - verifyHistoryButton() - verifyBookmarksButton() - verifySyncedTabsButton() - verifySettingsButton() - verifyFindInPageButton() - verifyAddFirefoxHome() - verifyAddToMobileHome() - verifyDesktopSite() - verifySaveCollection() - verifyAddBookmarkButton() - verifyShareButton() - verifyForwardButton() - verifyRefreshButton() + 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() + } } private fun assertShareTabsOverlay() { @@ -390,7 +403,8 @@ private fun assertSettingsButton() = settingsButton() .check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(isCompletelyDisplayed())) -private fun addOnsButton() = onView(allOf(withText("Add-ons"))) +private val addOnsText = if (FeatureFlags.toolbarMenuFeature) "Extensions" else "Add-ons" +private fun addOnsButton() = onView(allOf(withText(addOnsText))) private fun assertAddOnsButton() { onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) 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 a01593bb7..77303e932 100644 --- a/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt +++ b/app/src/geckoRelease/java/org/mozilla/fenix/engine/GeckoProvider.kt @@ -13,12 +13,18 @@ import mozilla.components.lib.crash.handler.CrashHandlerService import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate import org.mozilla.fenix.Config import org.mozilla.fenix.ext.components +import org.mozilla.geckoview.ContentBlocking import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntimeSettings +import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider object GeckoProvider { var testConfig: Bundle? = null private var runtime: GeckoRuntime? = null + const val CN_UPDATE_URL = + "https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2" + const val CN_GET_HASH_URL = + "https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2" @Synchronized fun getOrCreateRuntime( @@ -63,6 +69,28 @@ object GeckoProvider { runtimeSettings.fontSizeFactor = fontSize } + // Add safebrowsing providers for China + if (Config.channel.isMozillaOnline) { + val mozcn = SafeBrowsingProvider + .withName("mozcn") + .version("2.2") + .lists("m6eb-phish-shavar", "m6ib-phish-shavar") + .updateUrl(CN_UPDATE_URL) + .getHashUrl(CN_GET_HASH_URL) + .build() + + runtimeSettings.contentBlocking.setSafeBrowsingProviders(mozcn, + // Keep the existing configuration + ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER, + ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER) + + runtimeSettings.contentBlocking.setSafeBrowsingPhishingTable( + "m6eb-phish-shavar", + "m6ib-phish-shavar", + // Existing configuration + "goog-phish-proto") + } + val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val loginStorageDelegate = GeckoLoginStorageDelegate(storage) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efb94c3c0..b1b84e842 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ android:allowBackup="false" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" - android:requestLegacyExternalStorage="true" + android:requestLegacyExternalStorage="${requestLegacyExternalStorage}" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/NormalTheme" @@ -119,6 +119,9 @@ + + - - diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 1a9871792..ef40ffc3f 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -15,33 +15,17 @@ object FeatureFlags { const val pullToRefreshEnabled = true /** - * Shows Synced Tabs in the tabs tray. - * - * Tracking issue: https://github.com/mozilla-mobile/fenix/issues/13892 + * Enables the Nimbus experiments library. */ - val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug + val nimbusExperiments = Config.channel.isNightlyOrDebug /** - * Enables the Nimbus experiments library, especially the settings toggle to opt-out of - * all experiments. + * Enables WebAuthn support. */ - // IMPORTANT: Only turn this back on once the following issues are resolved: - // - https://github.com/mozilla-mobile/fenix/issues/17086: Calls to - // getExperimentBranch seem to block on updateExperiments causing a - // large performance regression loading the home screen. - // - https://github.com/mozilla-mobile/fenix/issues/17143: Despite - // having wrapped getExperimentBranch/withExperiments in a catch-all - // users are still experiencing crashes. - const val nimbusExperiments = false - - /** - * Enables the new MediaSession API. - */ - @Suppress("MayBeConst") - val newMediaSessionApi = true + val webAuthFeature = Config.channel.isNightlyOrDebug /** - * Enables experimental WebAuthn support. This implementation should never reach release! + * Shows new three-dot toolbar menu design. */ - val webAuthFeature = Config.channel.isNightlyOrDebug + val toolbarMenuFeature = Config.channel.isDebug } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index f76b5ffb3..0731fbb72 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -20,7 +20,6 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async import kotlinx.coroutines.launch import mozilla.appservices.Megazord -import mozilla.components.browser.session.Session import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.selector.selectedTab import mozilla.components.concept.base.crash.Breadcrumb @@ -30,6 +29,7 @@ import mozilla.components.lib.crash.CrashReporter import mozilla.components.service.glean.Glean import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.net.ConceptFetchHttpUploader +import mozilla.components.support.base.facts.register import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.ktx.android.content.isMainProcess @@ -39,9 +39,12 @@ import mozilla.components.support.rusthttp.RustHttpConfig import mozilla.components.support.rustlog.RustLog import mozilla.components.support.utils.logElapsedTime import mozilla.components.support.webextensions.WebExtensionSupport +import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.metrics.MetricServiceType +import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.runBlockingIncrement @@ -70,6 +73,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider { private set override fun onCreate() { + val methodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE. super.onCreate() setupInAllProcesses() @@ -91,6 +95,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider { } setupInMainProcessOnly() + + // We use start/stop instead of measure so we don't measure outside the main process. + PerfStartup.applicationOnCreate.stopAndAccumulate(methodDurationTimerId) // DO NOT MOVE ANYTHING BELOW HERE. } protected open fun initializeGlean() { @@ -119,6 +126,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider { @CallSuper open fun setupInMainProcessOnly() { + ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register() + run { // Attention: Do not invoke any code from a-s in this scope. val megazordSetup = setupMegazord() @@ -195,6 +204,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.core.bookmarksStorage.warmUp() components.core.passwordsStorage.warmUp() } + + SecurePrefsTelemetry(this@FenixApplication, components.analytics.experiments).startTests() } // Account manager initialization needs to happen on the main thread. GlobalScope.launch(Dispatchers.Main) { @@ -307,6 +318,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider { // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. RustHttpConfig.setClient(lazy { components.core.client }) RustLog.enable(components.analytics.crashReporter) + // We want to ensure Nimbus is initialized as early as possible so we can + // experiment on features close to startup. + // But we need viaduct (the RustHttp client) to be ready before we do. + components.analytics.experiments.initialize() } } @@ -417,9 +432,19 @@ open class FenixApplication : LocaleAwareApplication(), Provider { components.core.store.state.selectedTab?.content?.private ?: components.settings.openLinksInAPrivateTab - val session = Session(url, shouldCreatePrivateSession) - components.core.sessionManager.add(session, true, engineSession) - session.id + if (shouldCreatePrivateSession) { + components.useCases.tabsUseCases.addPrivateTab( + url = url, + selectTab = true, + engineSession = engineSession + ) + } else { + components.useCases.tabsUseCases.addTab( + url = url, + selectTab = true, + engineSession = engineSession + ) + } }, onCloseTabOverride = { _, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId) diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 64cfb7740..a648c64a7 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -17,7 +17,7 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewConfiguration -import android.view.WindowManager +import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.annotation.CallSuper import androidx.annotation.IdRes import androidx.annotation.VisibleForTesting @@ -52,6 +52,7 @@ import mozilla.components.feature.privatemode.notification.PrivateNotificationFe import mozilla.components.feature.search.BrowserStoreSearchAdapter import mozilla.components.feature.search.ext.legacy import mozilla.components.service.fxa.sync.SyncReason +import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.content.call @@ -64,6 +65,7 @@ import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.webextensions.WebExtensionPopupFeature import org.mozilla.fenix.GleanMetrics.Metrics +import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.addons.AddonDetailsFragmentDirections import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -91,6 +93,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.PerformanceInflater +import org.mozilla.fenix.perf.ProfilerMarkers import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.session.PrivateNotificationService @@ -161,7 +164,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { private lateinit var navigationToolbar: Toolbar - final override fun onCreate(savedInstanceState: Bundle?) { + final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measure { + // DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL. + components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity") + 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()) { @@ -277,6 +283,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { override fun onResume() { super.onResume() + // Even if screenshots are allowed, we hide private content in the recents screen in onPause + // so onResume we should go back to setting these flags with the user screenshot setting + // See https://github.com/mozilla-mobile/fenix/issues/11153 + updateSecureWindowFlags(settings().lastKnownMode) + // Diagnostic breadcrumb for "Display already aquired" crash: // https://github.com/mozilla-mobile/android-components/issues/7960 breadcrumb( @@ -318,6 +329,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { breadcrumb( message = "onStart()" ) + + ProfilerMarkers.homeActivityOnStart(rootContainer, components.core.engine.profiler) } override fun onStop() { @@ -340,8 +353,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { settings().shouldReturnToBrowser = components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty() + // Even if screenshots are allowed, we want to hide private content in the recents screen + // See https://github.com/mozilla-mobile/fenix/issues/11153 if (settings().lastKnownMode.isPrivate) { - window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + window.addFlags(FLAG_SECURE) } // We will remove this when AC code lands to emit a fact on getTopSites in DefaultTopSitesStorage @@ -536,6 +551,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { super.onBackPressed() } + final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach { + if (it is ActivityResultHandler && it.onActivityResult(requestCode, data, resultCode)) { + return + } + } + super.onActivityResult(requestCode, resultCode, data) + } + private fun shouldUseCustomBackLongPress(): Boolean { val isAndroidN = Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 @@ -840,7 +864,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager { return DefaultBrowsingModeManager(initialMode, components.settings) { newMode -> + updateSecureWindowFlags(newMode) themeManager.currentTheme = newMode + }.also { + updateSecureWindowFlags(initialMode) + } + } + + fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) { + if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) { + window.addFlags(FLAG_SECURE) + } else { + window.clearFlags(FLAG_SECURE) } } diff --git a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt index 3e84fa22a..0c995371e 100644 --- a/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/IntentReceiverActivity.kt @@ -26,6 +26,9 @@ class IntentReceiverActivity : Activity() { @VisibleForTesting override fun onCreate(savedInstanceState: Bundle?) { + // DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL. + components.core.engine.profiler?.addMarker("Activity.onCreate", "IntentReceiverActivity") + // StrictMode violation on certain devices such as Samsung components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt b/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt index c5c64df0a..53a55bdfd 100644 --- a/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt +++ b/app/src/main/java/org/mozilla/fenix/TelemetryMiddleware.kt @@ -8,17 +8,23 @@ import androidx.annotation.VisibleForTesting import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.EngineState +import mozilla.components.browser.state.state.SessionState import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.MiddlewareContext +import mozilla.components.support.base.android.Clock import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.GleanMetrics.Engine as EngineMetrics /** * [Middleware] to record telemetry in response to [BrowserAction]s. @@ -49,7 +55,7 @@ class TelemetryMiddleware( } } - @Suppress("TooGenericExceptionCaught", "ComplexMethod") + @Suppress("TooGenericExceptionCaught", "ComplexMethod", "NestedBlockDepth") override fun invoke( context: MiddlewareContext, next: (BrowserAction) -> Unit, @@ -60,8 +66,12 @@ class TelemetryMiddleware( is ContentAction.UpdateLoadingStateAction -> { context.state.findTab(action.sessionId)?.let { tab -> // Record UriOpened event when a non-private page finishes loading - if (tab.content.loading && !action.loading && !tab.content.private) { - metrics.track(Event.UriOpened) + if (tab.content.loading && !action.loading) { + if (!tab.content.private) { + metrics.track(Event.UriOpened) + } + + metrics.track(Event.NormalAndPrivateUriOpened) } } } @@ -90,6 +100,10 @@ class TelemetryMiddleware( is DownloadAction.AddDownloadAction -> { metrics.track(Event.DownloadAdded) } + is EngineAction.KillEngineSessionAction -> { + val tab = context.state.findTabOrCustomTab(action.sessionId) + onEngineSessionKilled(context.state, tab) + } } next(action) @@ -104,7 +118,44 @@ class TelemetryMiddleware( is TabListAction.RestoreAction -> { // Update/Persist tabs count whenever it changes settings.openTabsCount = context.state.normalTabs.count() + if (context.state.normalTabs.count() > 0) { + metrics.track(Event.HaveOpenTabs) + } else { + metrics.track(Event.HaveNoOpenTabs) + } } } } + + /** + * Collecting some engine-specific (GeckoView) telemetry. + * https://github.com/mozilla-mobile/android-components/issues/9366 + */ + private fun onEngineSessionKilled(state: BrowserState, tab: SessionState?) { + if (tab == null) { + logger.debug("Could not find tab for killed engine session") + return + } + + val isSelected = tab.id == state.selectedTabId + val ageNanos = tab.engineState.ageNanos() + + // Increment the counter of killed foreground/background tabs + val tabKillLabel = if (isSelected) { "foreground" } else { "background" } + EngineMetrics.tabKills[tabKillLabel].add() + + // Record the age of the engine session of the killed foreground/background tab. + if (isSelected && ageNanos != null) { + EngineMetrics.killForegroundAge.setRawNanos(ageNanos) + } else if (ageNanos != null) { + EngineMetrics.killBackgroundAge.setRawNanos(ageNanos) + } + } +} + +@Suppress("MagicNumber") +private fun EngineState.ageNanos(): Long? { + val timestamp = (timestamp ?: return null) + val now = Clock.elapsedRealtime() + return (now - timestamp) * 1_000_000 } 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 a98a767a9..64c77733d 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -38,6 +38,7 @@ import mozilla.components.feature.addons.ui.PermissionsDialogFragment import mozilla.components.feature.addons.ui.translateName import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter +import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.metrics.Event @@ -357,6 +358,7 @@ 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/InstalledAddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt index c3fbcce10..c154aa4f9 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt @@ -21,6 +21,7 @@ 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.ext.components @@ -127,6 +128,7 @@ class InstalledAddonDetailsFragment : Fragment() { ) ) } + Addons.hasEnabledAddons.set(true) } }, onError = { 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 60dfe78f3..ecc49681e 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.browser import android.content.Context import android.content.Intent +import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.Gravity @@ -37,13 +38,15 @@ import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mozilla.appservices.places.BookmarkRoot -import mozilla.components.browser.session.Session import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.findTabOrCustomTab import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.content.DownloadState @@ -57,6 +60,7 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.manager.FetchDownloadManager +import mozilla.components.feature.downloads.share.ShareDownloadFeature import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature import mozilla.components.feature.privatemode.feature.SecureWindowFeature @@ -68,7 +72,6 @@ import mozilla.components.feature.session.FullScreenFeature import mozilla.components.feature.session.PictureInPictureFeature import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SwipeRefreshFeature -import mozilla.components.feature.session.behavior.EngineViewBottomBehavior import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.lib.state.ext.consumeFlow @@ -81,6 +84,7 @@ 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 +import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity @@ -100,9 +104,7 @@ import org.mozilla.fenix.components.toolbar.BrowserToolbarView import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController -import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior import org.mozilla.fenix.components.toolbar.ToolbarIntegration -import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.ext.accessibilityManager @@ -124,8 +126,10 @@ import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import java.lang.ref.WeakReference -import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi +import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior +import mozilla.components.feature.webauthn.WebAuthnFeature +import mozilla.components.support.base.feature.ActivityResultHandler +import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition /** * Base fragment extended by [BrowserFragment]. @@ -134,7 +138,7 @@ import org.mozilla.fenix.FeatureFlags.newMediaSessionApi */ @ExperimentalCoroutinesApi @Suppress("TooManyFunctions", "LargeClass") -abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, +abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler, OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { private lateinit var browserFragmentStore: BrowserFragmentStore @@ -157,6 +161,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, private val sessionFeature = ViewBoundFeatureWrapper() private val contextMenuFeature = ViewBoundFeatureWrapper() private val downloadsFeature = ViewBoundFeatureWrapper() + private val shareDownloadsFeature = ViewBoundFeatureWrapper() private val appLinksFeature = ViewBoundFeatureWrapper() private val promptsFeature = ViewBoundFeatureWrapper() private val findInPageIntegration = ViewBoundFeatureWrapper() @@ -168,8 +173,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, private val sitePermissionWifiIntegration = ViewBoundFeatureWrapper() private val secureWindowFeature = ViewBoundFeatureWrapper() - private var fullScreenMediaFeature = - ViewBoundFeatureWrapper() private var fullScreenMediaSessionFeature = ViewBoundFeatureWrapper() private val searchFeature = ViewBoundFeatureWrapper() @@ -253,7 +256,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, @CallSuper internal open fun initializeUI(view: View, tab: SessionState) { val context = requireContext() - val sessionManager = context.components.core.sessionManager val store = context.components.core.store val activity = requireActivity() as HomeActivity @@ -280,14 +282,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ) val browserToolbarController = DefaultBrowserToolbarController( store = store, + tabsUseCases = requireComponents.useCases.tabsUseCases, activity = activity, navController = findNavController(), metrics = requireComponents.analytics.metrics, readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, engineView = engineView, homeViewModel = homeViewModel, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + customTabSessionId = customTabSessionId, onTabCounterClicked = { thumbnailsFeature.get()?.requestScreenshot() findNavController().nav( @@ -317,17 +319,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } ) val browserToolbarMenuController = DefaultBrowserToolbarMenuController( + store = store, activity = activity, navController = findNavController(), metrics = requireComponents.analytics.metrics, settings = context.settings(), readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, sessionFeature = sessionFeature, findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, swipeRefresh = swipeRefresh, browserAnimator = browserAnimator, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + customTabSessionId = customTabSessionId, openInFenixIntent = openInFenixIntent, bookmarkTapped = { url: String, title: String -> viewLifecycleOwner.lifecycleScope.launch { @@ -349,7 +351,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, container = view.browserLayout, toolbarPosition = context.settings().toolbarPosition, interactor = browserInteractor, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) }, lifecycleOwner = viewLifecycleOwner ) @@ -405,25 +407,21 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, view = view ) - if (newMediaSessionApi) { - fullScreenMediaSessionFeature.set( - feature = MediaSessionFullscreenFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view - ) - } else { - fullScreenMediaFeature.set( - feature = MediaFullscreenOrientationFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view - ) - } + fullScreenMediaSessionFeature.set( + feature = MediaSessionFullscreenFeature( + requireActivity(), + context.components.core.store + ), + owner = this, + view = view + ) + + val shareDownloadFeature = ShareDownloadFeature( + context = context.applicationContext, + httpClient = context.components.core.client, + store = store, + tabId = customTabSessionId + ) val downloadFeature = DownloadsFeature( context.applicationContext, @@ -461,9 +459,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> // If the download is just paused, don't show any in-app notification - if (downloadJobStatus == DownloadState.Status.COMPLETED || - downloadJobStatus == DownloadState.Status.FAILED - ) { + if (shouldShowCompletedDownloadDialog(downloadState, downloadJobStatus)) { saveDownloadDialogState( downloadState.sessionId, @@ -491,19 +487,22 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } ) - // Don't show the dialog if we aren't in the tab that started the download - if (downloadState.sessionId == sessionManager.selectedSession?.id) { - dynamicDownloadDialog.show() - browserToolbarView.expand() - } + dynamicDownloadDialog.show() + browserToolbarView.expand() } } resumeDownloadDialogState( - sessionManager.selectedSession?.id, + getCurrentTab()?.id, store, view, context, toolbarHeight ) + shareDownloadsFeature.set( + shareDownloadFeature, + owner = this, + view = view + ) + downloadsFeature.set( downloadFeature, owner = this, @@ -532,7 +531,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, promptsFeature.set( feature = PromptFeature( - fragment = this, + activity = activity, store = store, customTabId = customTabSessionId, fragmentManager = parentFragmentManager, @@ -553,7 +552,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, val directions = NavGraphDirections.actionGlobalShareFragment( data = arrayOf(shareData), showPage = true, - sessionId = getSessionById()?.id + sessionId = getCurrentTab()?.id ) findNavController().navigate(directions) } @@ -587,14 +586,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, searchFeature.set( feature = SearchFeature(store, customTabSessionId) { request, tabId -> - val parentSession = sessionManager.findSessionById(tabId) + val parentSession = store.state.findTabOrCustomTab(tabId) val useCase = if (request.isPrivate) { requireComponents.useCases.searchUseCases.newPrivateTabSearch } else { requireComponents.useCases.searchUseCases.newTabSearch } - if (parentSession?.isCustomTabSession() == true) { + if (parentSession is CustomTabSessionState) { useCase.invoke(request.query) requireActivity().startActivity(openInFenixIntent) } else { @@ -643,7 +642,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, view = view ) - if (FeatureFlags.webAuthFeature) { + // This component feature only works on Fenix when built on Mozilla infrastructure. + if (FeatureFlags.webAuthFeature && BuildConfig.MOZILLA_OFFICIAL) { webAuthnFeature.set( feature = WebAuthnFeature( engine = requireComponents.core.engine, @@ -818,34 +818,37 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, !inFullScreen } - private fun initializeEngineView(toolbarHeight: Int) { + @VisibleForTesting + internal fun initializeEngineView(toolbarHeight: Int) { val context = requireContext() - if (context.settings().isDynamicToolbarEnabled) { - engineView.setDynamicToolbarMaxHeight(toolbarHeight) + if (!context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled) { + getEngineView().setDynamicToolbarMaxHeight(toolbarHeight) - val behavior = when (context.settings().toolbarPosition) { - // Set engineView dynamic vertical clipping depending on the toolbar position. - ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null) - // Set scroll flags depending on if if the browser or the website is doing the scroll. - ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior( + val toolbarPosition = if (context.settings().shouldUseBottomToolbar) { + MozacToolbarPosition.BOTTOM + } else { + MozacToolbarPosition.TOP + } + (getSwipeRefreshLayout().layoutParams as CoordinatorLayout.LayoutParams).behavior = + EngineViewBrowserToolbarBehavior( context, null, - engineView, - browserToolbarView + getSwipeRefreshLayout(), + toolbarHeight, + toolbarPosition ) - } - - (swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior } else { // Ensure webpage's bottom elements are aligned to the very bottom of the engineView. - engineView.setDynamicToolbarMaxHeight(0) + getEngineView().setDynamicToolbarMaxHeight(0) - // Effectively place the engineView on top of the toolbar if that is not dynamic. + // Effectively place the engineView on top/below of the toolbar if that is not dynamic. + val swipeRefreshParams = + getSwipeRefreshLayout().layoutParams as CoordinatorLayout.LayoutParams if (context.settings().shouldUseBottomToolbar) { - val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams - browserEngine.bottomMargin = - requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) + swipeRefreshParams.bottomMargin = toolbarHeight + } else { + swipeRefreshParams.topMargin = toolbarHeight } } } @@ -1009,7 +1012,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, final override fun onViewStateRestored(savedInstanceState: Bundle?) { super.onViewStateRestored(savedInstanceState) savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let { - if (requireComponents.core.sessionManager.findSessionById(it)?.customTabConfig != null) { + if (requireComponents.core.store.state.findCustomTab(it) != null) { customTabSessionId = it } } @@ -1033,10 +1036,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } /** - * Forwards activity results to the prompt feature. + * Forwards activity results to the [ActivityResultHandler] features. */ - final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - listOf( + override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean { + return listOf( promptsFeature, webAuthnFeature ).any { it.onActivityResult(requestCode, data, resultCode) } @@ -1047,22 +1050,18 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, * or if it has a parent session and no more history */ protected open fun removeSessionIfNeeded(): Boolean { - getSessionById()?.let { session -> + getCurrentTab()?.let { session -> return if (session.source == SessionState.Source.ACTION_VIEW) { activity?.finish() - requireComponents.useCases.tabsUseCases.removeTab(session) + requireComponents.useCases.tabsUseCases.removeTab(session.id) true } else { - if (session.hasParentSession) { - // The removeTab use case does not currently select a parent session, so - // we are using sessionManager.remove - requireComponents.core.sessionManager.remove( - session, - selectParentIfExists = true - ) + val hasParentSession = session is TabSessionState && session.parentId != null + if (hasParentSession) { + requireComponents.useCases.tabsUseCases.removeTab(session.id, selectParentIfExists = true) } // We want to return to home if this session didn't have a parent session to select. - val goToOverview = !session.hasParentSession + val goToOverview = !hasParentSession !goToOverview } } @@ -1127,20 +1126,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, (activity as HomeActivity).browsingModeManager.mode = sessionMode } - /** - * Returns the current session. - */ - protected fun getSessionById(): Session? { - val sessionManager = requireComponents.core.sessionManager - val localCustomTabId = customTabSessionId - return if (localCustomTabId != null) { - sessionManager.findSessionById(localCustomTabId) - } else { - sessionManager.selectedSession - } - } - - private fun getCurrentTab(): SessionState? { + @VisibleForTesting + internal fun getCurrentTab(): SessionState? { return requireComponents.core.store.state.findCustomTabOrSelectedTab(customTabSessionId) } @@ -1228,12 +1215,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, .setText(getString(R.string.full_screen_notification)) .show() activity?.enterToImmersiveMode() + browserToolbarView.collapse() browserToolbarView.view.isVisible = false val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams browserEngine.bottomMargin = 0 + browserEngine.topMargin = 0 + swipeRefresh.translationY = 0f engineView.setDynamicToolbarMaxHeight(0) - browserToolbarView.expand() // Without this, fullscreen has a margin at the top. engineView.setVerticalClipping(0) @@ -1247,6 +1236,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, browserToolbarView.view.isVisible = true val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) initializeEngineView(toolbarHeight) + browserToolbarView.expand() } } @@ -1310,10 +1300,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, override fun onAccessibilityStateChanged(enabled: Boolean) { if (_browserToolbarView != null) { - browserToolbarView.setScrollFlags(enabled) + browserToolbarView.setToolbarBehavior(enabled) } } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + _browserToolbarView?.dismissMenu() + } + // This method is called in response to native web extension messages from // content scripts (e.g the reader view extension). By the time these // messages are processed the fragment/view may no longer be attached. @@ -1326,4 +1322,28 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } } } + + /** + * Convenience method for replacing EngineView (id/engineView) in unit tests. + */ + @VisibleForTesting + internal fun getEngineView() = engineView + + /** + * Convenience method for replacing SwipeRefreshLayout (id/swipeRefresh) in unit tests. + */ + @VisibleForTesting + internal fun getSwipeRefreshLayout() = swipeRefresh + + @VisibleForTesting + internal fun shouldShowCompletedDownloadDialog( + downloadState: DownloadState, + status: DownloadState.Status + ): Boolean { + + val isValidStatus = status in listOf(DownloadState.Status.COMPLETED, DownloadState.Status.FAILED) + val isSameTab = downloadState.sessionId == getCurrentTab()?.id ?: false + + return isValidStatus && isSameTab + } } 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 042d33a3b..58515a908 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -69,7 +69,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { tabPreview = tabPreview, toolbarLayout = browserToolbarView.view, store = components.core.store, - sessionManager = components.core.sessionManager + selectTabUseCase = components.useCases.tabsUseCases.selectTab ) ) } @@ -84,7 +84,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { visible = { readerModeAvailable }, - selected = getSessionById()?.let { + selected = getCurrentTab()?.let { activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active } ?: false, listener = browserInteractor::onReaderModePressed @@ -137,7 +137,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { navController = findNavController(), settings = context.settings(), appLinksUseCases = context.components.useCases.appLinksUseCases, - container = browserLayout as ViewGroup + container = browserLayout as ViewGroup, + shouldScrollWithTopToolbar = !context.settings().shouldUseBottomToolbar ), owner = this, view = view diff --git a/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt b/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt index ed96a50cd..4753360c3 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/CustomTabContextMenuCandidate.kt @@ -29,6 +29,7 @@ class CustomTabContextMenuCandidate { ), ContextMenuCandidate.createShareLinkCandidate(context), ContextMenuCandidate.createSaveImageCandidate(context, contextMenuUseCases), + ContextMenuCandidate.createSaveVideoAudioCandidate(context, contextMenuUseCases), ContextMenuCandidate.createCopyImageLocationCandidate( context, snackBarParentView, diff --git a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt index 1d129a027..9205a26bd 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt @@ -22,6 +22,11 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import org.mozilla.fenix.R +import org.mozilla.fenix.browser.infobanner.DynamicInfoBanner +import org.mozilla.fenix.browser.infobanner.InfoBanner +import org.mozilla.fenix.components.metrics.Event +import org.mozilla.fenix.components.metrics.Event.BannerOpenInAppGoToSettings +import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.nav import org.mozilla.fenix.utils.Settings @@ -37,7 +42,9 @@ class OpenInAppOnboardingObserver( private val navController: NavController, private val settings: Settings, private val appLinksUseCases: AppLinksUseCases, - private val container: ViewGroup + private val container: ViewGroup, + @VisibleForTesting + internal val shouldScrollWithTopToolbar: Boolean = false ) : LifecycleAwareFeature { private var scope: CoroutineScope? = null private var currentUrl: String? = null @@ -85,22 +92,30 @@ class OpenInAppOnboardingObserver( infoBanner?.showBanner() sessionDomainForDisplayedBanner = url.tryGetHostFromUrl() settings.shouldShowOpenInAppBanner = false + context.components.analytics.metrics.track(Event.BannerOpenInAppDisplayed) } } @VisibleForTesting - internal fun createInfoBanner(): InfoBanner { - return InfoBanner( + internal fun createInfoBanner(): DynamicInfoBanner { + return DynamicInfoBanner( context = context, message = context.getString(R.string.open_in_app_cfr_info_message), dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text), actionText = context.getString(R.string.open_in_app_cfr_positive_button_text), - container = container + container = container, + shouldScrollWithTopToolbar = shouldScrollWithTopToolbar, + dismissAction = ::dismissAction ) { val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment( preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app) ) + context.components.analytics.metrics.track(BannerOpenInAppGoToSettings) navController.nav(R.id.browserFragment, directions) } } + + private fun dismissAction() { + context.components.analytics.metrics.track(Event.BannerOpenInAppDismissed) + } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt index da19ec785..d6284bc4b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -17,16 +17,16 @@ import androidx.core.graphics.contains import androidx.core.graphics.toPoint import androidx.core.view.isVisible import androidx.interpolator.view.animation.LinearOutSlowInInterpolator -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.ktx.android.view.getRectWithViewLocation import org.mozilla.fenix.R import org.mozilla.fenix.ext.getRectWithScreenLocation import org.mozilla.fenix.ext.getWindowInsets import org.mozilla.fenix.ext.isKeyboardVisible -import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import kotlin.math.abs import kotlin.math.max @@ -43,7 +43,7 @@ class ToolbarGestureHandler( private val tabPreview: TabPreview, private val toolbarLayout: View, private val store: BrowserStore, - private val sessionManager: SessionManager + private val selectTabUseCase: TabsUseCases.SelectTabUseCase ) : SwipeGestureListener { private enum class GestureDirection { @@ -51,7 +51,7 @@ class ToolbarGestureHandler( } private sealed class Destination { - data class Tab(val session: Session) : Destination() + data class Tab(val tab: TabSessionState) : Destination() object None : Destination() } @@ -140,7 +140,7 @@ class ToolbarGestureHandler( ) { val destination = getDestination() if (destination is Destination.Tab && isGestureComplete(velocityX)) { - animateToNextTab(destination.session) + animateToNextTab(destination.tab) } else { animateCanceledGesture(velocityX) } @@ -149,14 +149,14 @@ class ToolbarGestureHandler( private fun getDestination(): Destination { val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR val currentTab = store.state.selectedTab ?: return Destination.None - val currentIndex = sessionManager.sessionsOfType(currentTab.content.private).indexOfFirst { + val currentIndex = store.state.getNormalOrPrivateTabs(currentTab.content.private).indexOfFirst { it.id == currentTab.id } return if (currentIndex == -1) { Destination.None } else { - val sessions = sessionManager.sessionsOfType(currentTab.content.private) + val tabs = store.state.getNormalOrPrivateTabs(currentTab.content.private) val index = when (gestureDirection) { GestureDirection.RIGHT_TO_LEFT -> if (isLtr) { currentIndex + 1 @@ -170,8 +170,8 @@ class ToolbarGestureHandler( } } - if (index < sessions.count() && index >= 0) { - Destination.Tab(sessions.elementAt(index)) + if (index < tabs.count() && index >= 0) { + Destination.Tab(tabs.elementAt(index)) } else { Destination.None } @@ -180,7 +180,7 @@ class ToolbarGestureHandler( private fun preparePreview(destination: Destination) { val thumbnailId = when (destination) { - is Destination.Tab -> destination.session.id + is Destination.Tab -> destination.tab.id is Destination.None -> return } @@ -233,7 +233,7 @@ class ToolbarGestureHandler( } } - private fun animateToNextTab(session: Session) { + private fun animateToNextTab(tab: TabSessionState) { val browserFinalXCoordinate: Float = when (gestureDirection) { GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset @@ -243,7 +243,7 @@ class ToolbarGestureHandler( getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply { doOnEnd { contentLayout.translationX = 0f - sessionManager.select(session) + selectTabUseCase(tab.id) // Fade out the tab preview to prevent flickering val shortAnimationDuration = diff --git a/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt b/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt deleted file mode 100644 index 24576f932..000000000 --- a/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt +++ /dev/null @@ -1,62 +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.browser - -import android.app.Activity -import android.content.Intent -import android.content.IntentSender -import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.activity.ActivityDelegate -import mozilla.components.support.base.feature.ActivityResultHandler -import mozilla.components.support.base.feature.LifecycleAwareFeature -import mozilla.components.support.base.log.logger.Logger -import org.mozilla.fenix.FeatureFlags - -/** - * This implementation of the WebAuthnFeature is only for testing in a nightly signed build. - * - * ⚠️ This should always be behind the [FeatureFlags.webAuthFeature] nightly flag. - */ -class WebAuthnFeature( - private val engine: Engine, - private val activity: Activity -) : LifecycleAwareFeature, ActivityResultHandler { - val logger = Logger("WebAuthnFeature") - var requestCode = ACTIVITY_REQUEST_CODE - var resultCallback: ((Intent?) -> Unit)? = null - private val delegate = object : ActivityDelegate { - override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) { - val code = requestCode++ - logger.info("Received activity delegate request with code: $code intent: $intent") - activity.startIntentSenderForResult(intent, code, null, 0, 0, 0) - resultCallback = onResult - } - } - - override fun start() { - logger.info("Feature started.") - engine.registerActivityDelegate(delegate) - } - - override fun stop() { - logger.info("Feature stopped.") - engine.unregisterActivityDelegate() - } - - override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean { - logger.info("Received activity result with code: $requestCode\ndata: $data") - if (this.requestCode == requestCode) { - logger.info("Invoking callback!") - resultCallback?.invoke(data) - return true - } - - return false - } - - companion object { - const val ACTIVITY_REQUEST_CODE = 10 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.kt new file mode 100644 index 000000000..83e11465c --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBanner.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.browser.infobanner + +import android.content.Context +import android.view.ViewGroup +import androidx.annotation.VisibleForTesting +import androidx.coordinatorlayout.widget.CoordinatorLayout + +/** + * [InfoBanner] that will automatically scroll with the top [BrowserToolbar]. + * Only to be used with [BrowserToolbar]s placed at the top of the screen. + * + * @param shouldScrollWithTopToolbar whether to follow the Y translation of the top toolbar or not + */ +@Suppress("LongParameterList") +class DynamicInfoBanner( + private val context: Context, + container: ViewGroup, + @VisibleForTesting + internal val shouldScrollWithTopToolbar: Boolean = false, + message: String, + dismissText: String, + actionText: String? = null, + dismissByHiding: Boolean = false, + dismissAction: (() -> Unit)? = null, + actionToPerform: (() -> Unit)? = null +) : InfoBanner( + context, container, message, dismissText, actionText, dismissByHiding, dismissAction, actionToPerform +) { + override fun showBanner() { + super.showBanner() + + if (shouldScrollWithTopToolbar) { + (bannerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior = DynamicInfoBannerBehavior( + context, null + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehavior.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehavior.kt new file mode 100644 index 000000000..752826248 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehavior.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.browser.infobanner + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.annotation.VisibleForTesting +import androidx.coordinatorlayout.widget.CoordinatorLayout +import mozilla.components.browser.toolbar.BrowserToolbar + +/** + * A [CoordinatorLayout.Behavior] implementation to be used when placing [InfoBanner] + * below the BrowserToolbar with which is has to scroll. + * + * This Behavior will keep the Y translations of [InfoBanner] and the top [BrowserToolbar] in sync + * so that the banner will be shown between: + * - the top of the container, being translated over the initial toolbar height (toolbar fully collapsed) + * - immediately below the toolbar (toolbar fully expanded). + */ +class DynamicInfoBannerBehavior( + context: Context?, + attrs: AttributeSet? +) : CoordinatorLayout.Behavior(context, attrs) { + @VisibleForTesting + internal var toolbarHeight: Int = 0 + + override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + if (dependency::class == BrowserToolbar::class) { + toolbarHeight = dependency.height + setBannerYTranslation(child, dependency.translationY) + return true + } + + return super.layoutDependsOn(parent, child, dependency) + } + + override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean { + setBannerYTranslation(child, dependency.translationY) + + return true + } + + @VisibleForTesting + internal fun setBannerYTranslation(banner: View, newYTranslation: Float) { + banner.translationY = toolbarHeight + newYTranslation + } +} diff --git a/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt similarity index 85% rename from app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt rename to app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt index d7f4187cc..1d1ba4041 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/InfoBanner.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/infobanner/InfoBanner.kt @@ -2,15 +2,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/. */ -package org.mozilla.fenix.browser +package org.mozilla.fenix.browser.infobanner import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.View.GONE import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams.MATCH_PARENT -import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import androidx.annotation.VisibleForTesting import kotlinx.android.synthetic.main.info_banner.view.* import org.mozilla.fenix.R import org.mozilla.fenix.ext.settings @@ -27,7 +26,7 @@ import org.mozilla.fenix.ext.settings * @param actionToPerform - The action to be performed on action button press */ @SuppressWarnings("LongParameterList") -class InfoBanner( +open class InfoBanner( private val context: Context, private val container: ViewGroup, private val message: String, @@ -38,10 +37,11 @@ class InfoBanner( private val actionToPerform: (() -> Unit)? = null ) { @SuppressLint("InflateParams") - private val bannerLayout = LayoutInflater.from(context) + @VisibleForTesting + internal val bannerLayout = LayoutInflater.from(context) .inflate(R.layout.info_banner, null) - internal fun showBanner() { + internal open fun showBanner() { bannerLayout.banner_info_message.text = message bannerLayout.dismiss.text = dismissText @@ -53,10 +53,6 @@ class InfoBanner( container.addView(bannerLayout) - val params = bannerLayout.layoutParams as ViewGroup.LayoutParams - params.height = WRAP_CONTENT - params.width = MATCH_PARENT - bannerLayout.dismiss.setOnClickListener { dismissAction?.invoke() if (dismissByHiding) { bannerLayout.visibility = GONE } else { dismiss() } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt index c9fffedb8..8e45a1d72 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationStore.kt @@ -15,7 +15,6 @@ import mozilla.components.lib.state.State import mozilla.components.lib.state.Store import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged import org.mozilla.fenix.components.TabCollectionStorage -import org.mozilla.fenix.ext.getMediaStateForSession import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.home.Tab @@ -87,12 +86,11 @@ internal fun BrowserState.getTabs( ): List { return tabIds ?.mapNotNull { id -> findTab(id) } - ?.map { it.toTab(this, publicSuffixList) } + ?.map { it.toTab(publicSuffixList) } .orEmpty() } private fun TabSessionState.toTab( - state: BrowserState, publicSuffixList: PublicSuffixList ): Tab { val url = readerState.activeUrl ?: content.url @@ -102,8 +100,7 @@ private fun TabSessionState.toTab( hostname = url.toShortUrl(publicSuffixList), title = content.title, selected = null, - icon = content.icon, - mediaState = state.getMediaStateForSession(this.id) + icon = content.icon ) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt index 0edcb6c29..7b23249a9 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationView.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.collections import android.os.Handler +import android.os.Looper import android.text.InputFilter import android.view.KeyEvent import android.view.LayoutInflater @@ -19,7 +20,6 @@ import androidx.transition.Transition import androidx.transition.TransitionManager import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.component_collection_creation.* -import mozilla.components.browser.state.state.MediaState import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.showKeyboard @@ -169,7 +169,7 @@ class CollectionCreationView( text = context.getString(R.string.create_collection_name_collection) setOnClickListener { name_collection_edittext.hideKeyboard() - val handler = Handler() + val handler = Handler(Looper.getMainLooper()) handler.postDelayed({ interactor.onBackPressed(SaveCollectionStep.NameCollection) }, TRANSITION_DURATION) @@ -197,8 +197,7 @@ class CollectionCreationView( sessionId = tab.id.toString(), url = tab.url, hostname = tab.url.toShortUrl(publicSuffixList), - title = tab.title, - mediaState = MediaState.State.NONE + title = tab.title ) }.let { tabs -> collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true) @@ -216,7 +215,7 @@ class CollectionCreationView( text = context.getString(R.string.collection_rename) setOnClickListener { name_collection_edittext.hideKeyboard() - val handler = Handler() + val handler = Handler(Looper.getMainLooper()) handler.postDelayed({ interactor.onBackPressed(SaveCollectionStep.RenameCollection) }, TRANSITION_DURATION) 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 e0d49dd17..9197287b8 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Analytics.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Analytics.kt @@ -24,6 +24,7 @@ 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.experiments.createNimbus import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.perf.lazyMonitored 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 73c8038a6..52ba15e31 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -56,6 +56,7 @@ class Components(private val context: Context) { } val services by lazyMonitored { Services(context, backgroundServices.accountManager) } val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) } + @Suppress("Deprecation") val useCases by lazyMonitored { UseCases( context, @@ -66,6 +67,7 @@ class Components(private val context: Context) { core.topSitesStorage ) } + @Suppress("Deprecation") val intentProcessors by lazyMonitored { IntentProcessors( context, diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 96b6b504a..41dfa2543 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -18,7 +18,6 @@ import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.storage.SessionStorage -import mozilla.components.browser.session.undo.UndoMiddleware import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.storage.sync.PlacesBookmarksStorage @@ -35,7 +34,6 @@ import mozilla.components.feature.customtabs.store.CustomTabsServiceStore 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.MediaMiddleware import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.WebAppShortcutManager @@ -44,6 +42,8 @@ import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware import mozilla.components.feature.search.middleware.SearchMiddleware import mozilla.components.feature.search.region.RegionMiddleware import mozilla.components.feature.session.HistoryDelegate +import mozilla.components.feature.session.middleware.LastAccessMiddleware +import mozilla.components.feature.session.middleware.undo.UndoMiddleware import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.PinnedSiteStorage import mozilla.components.feature.webcompat.WebCompatFeature @@ -61,7 +61,6 @@ import mozilla.components.support.locale.LocaleManager import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.TelemetryMiddleware @@ -69,7 +68,6 @@ import org.mozilla.fenix.components.search.SearchMigration import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings -import org.mozilla.fenix.media.MediaService import org.mozilla.fenix.media.MediaSessionService import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.lazyMonitored @@ -178,6 +176,7 @@ class Core( val store by lazyMonitored { val middlewareList = mutableListOf( + LastAccessMiddleware(), RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine), DownloadMiddleware(context, DownloadService::class.java), ReaderViewMiddleware(), @@ -197,19 +196,17 @@ class Core( RecordingDevicesMiddleware(context) ) - if (!newMediaSessionApi) { - middlewareList.add(MediaMiddleware(context, MediaService::class.java)) - } - BrowserStore( middleware = middlewareList + EngineMiddleware.create(engine, ::findSessionById) ) } + @Suppress("Deprecation") private fun lookupSessionManager(): SessionManager { return sessionManager } + @Suppress("Deprecation") private fun findSessionById(tabId: String): Session? { return sessionManager.findSessionById(tabId) } @@ -232,6 +229,7 @@ class Core( * sessions from the [SessionStorage], and with a default session (about:blank) in * case all sessions/tabs are closed. */ + @Deprecated("Use browser store (for reading) and use cases (for writing) instead") val sessionManager by lazyMonitored { SessionManager(engine, store).also { // Install the "icons" WebExtension to automatically load icons for every visited website. @@ -248,9 +246,7 @@ class Core( permissionStorage.permissionsStorage, HomeActivity::class.java ) - if (newMediaSessionApi) { - MediaSessionFeature(context, MediaSessionService::class.java, store).start() - } + MediaSessionFeature(context, MediaSessionService::class.java, store).start() } } 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 cb475e7e5..1f0b41d4c 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 @@ -9,6 +9,7 @@ import mozilla.components.browser.errorpages.ErrorType import mozilla.components.browser.state.search.SearchEngine import mozilla.components.feature.top.sites.TopSite import org.mozilla.fenix.GleanMetrics.Addons +import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.Collections @@ -52,6 +53,7 @@ sealed class Event { object CustomTabsActionTapped : Event() object CustomTabsMenuOpened : Event() object UriOpened : Event() + object NormalAndPrivateUriOpened : Event() object SyncAuthOpened : Event() object SyncAuthClosed : Event() object SyncAuthSignUp : Event() @@ -198,12 +200,21 @@ sealed class Event { object SyncedTabOpened : Event() object RecentlyClosedTabsOpened : Event() + object HaveOpenTabs : Event() + object HaveNoOpenTabs : Event() + + object BannerOpenInAppDisplayed : Event() + object BannerOpenInAppDismissed : Event() + object BannerOpenInAppGoToSettings : Event() object ContextMenuCopyTapped : Event() object ContextMenuSearchTapped : Event() object ContextMenuSelectAllTapped : Event() object ContextMenuShareTapped : Event() + object HaveTopSites : Event() + object HaveNoTopSites : Event() + // Interaction events with extras data class TopSiteSwipeCarousel(val page: Int) : Event() { @@ -211,6 +222,25 @@ sealed class Event { get() = hashMapOf(TopSites.swipeCarouselKeys.page to page.toString()) } + data class SecurePrefsExperimentFailure(val failureException: String) : Event() { + override val extras = + mapOf(AndroidKeystoreExperiment.experimentFailureKeys.failureException to failureException) + } + data class SecurePrefsGetFailure(val failureException: String) : Event() { + override val extras = + mapOf(AndroidKeystoreExperiment.getFailureKeys.failureException to failureException) + } + data class SecurePrefsGetSuccess(val successCode: String) : Event() { + override val extras = + mapOf(AndroidKeystoreExperiment.getResultKeys.result to successCode) + } + data class SecurePrefsWriteFailure(val failureException: String) : Event() { + override val extras = + mapOf(AndroidKeystoreExperiment.writeFailureKeys.failureException to failureException) + } + object SecurePrefsWriteSuccess : Event() + object SecurePrefsReset : Event() + data class TopSiteLongPress(val type: TopSite.Type) : Event() { override val extras: Map? get() = hashMapOf(TopSites.longPressKeys.type to type.name) 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 af9cb5a4a..d28e40d21 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 @@ -15,8 +15,10 @@ import mozilla.components.support.base.log.logger.Logger import org.mozilla.fenix.Config import org.mozilla.fenix.GleanMetrics.AboutPage import org.mozilla.fenix.GleanMetrics.Addons +import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.Autoplay +import org.mozilla.fenix.GleanMetrics.BannerOpenInApp import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BrowserSearch import org.mozilla.fenix.GleanMetrics.Collections @@ -236,6 +238,9 @@ private val Event.wrapper: EventWrapper<*>? is Event.UriOpened -> EventWrapper( { Events.totalUriCount.add(1) } ) + is Event.NormalAndPrivateUriOpened -> EventWrapper( + { Events.normalAndPrivateUriCount.add(1) } + ) is Event.ErrorPageVisited -> EventWrapper( { ErrorPage.visitedError.record(it) }, { ErrorPage.visitedErrorKeys.valueOf(it) } @@ -723,6 +728,51 @@ private val Event.wrapper: EventWrapper<*>? is Event.ContextMenuShareTapped -> EventWrapper( { ContextualMenu.shareTapped.record(it) } ) + Event.HaveOpenTabs -> EventWrapper( + { Metrics.hasOpenTabs.set(true) } + ) + Event.HaveNoOpenTabs -> EventWrapper( + { Metrics.hasOpenTabs.set(false) } + ) + Event.HaveTopSites -> EventWrapper( + { Metrics.hasTopSites.set(true) } + ) + Event.HaveNoTopSites -> EventWrapper( + { Metrics.hasTopSites.set(false) } + ) + + is Event.BannerOpenInAppDisplayed -> EventWrapper( + { BannerOpenInApp.displayed.record(it) } + ) + is Event.BannerOpenInAppDismissed -> EventWrapper( + { BannerOpenInApp.dismissed.record(it) } + ) + is Event.BannerOpenInAppGoToSettings -> EventWrapper( + { BannerOpenInApp.goToSettings.record(it) } + ) + + is Event.SecurePrefsExperimentFailure -> EventWrapper( + { AndroidKeystoreExperiment.experimentFailure.record(it) }, + { AndroidKeystoreExperiment.experimentFailureKeys.valueOf(it) } + ) + is Event.SecurePrefsGetFailure -> EventWrapper( + { AndroidKeystoreExperiment.getFailure.record(it) }, + { AndroidKeystoreExperiment.getFailureKeys.valueOf(it) } + ) + is Event.SecurePrefsGetSuccess -> EventWrapper( + { AndroidKeystoreExperiment.getResult.record(it) }, + { AndroidKeystoreExperiment.getResultKeys.valueOf(it) } + ) + is Event.SecurePrefsWriteFailure -> EventWrapper( + { AndroidKeystoreExperiment.writeFailure.record(it) }, + { AndroidKeystoreExperiment.writeFailureKeys.valueOf(it) } + ) + is Event.SecurePrefsWriteSuccess -> EventWrapper( + { AndroidKeystoreExperiment.writeSuccess.record(it) } + ) + is Event.SecurePrefsReset -> EventWrapper( + { AndroidKeystoreExperiment.reset.record(it) } + ) // Don't record other events in Glean: is Event.AddBookmark -> null 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 3b31385c2..7f9aed10a 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 @@ -22,6 +22,8 @@ import mozilla.components.feature.findinpage.facts.FindInPageFacts import mozilla.components.feature.media.facts.MediaFacts import mozilla.components.feature.prompts.dialog.LoginDialogFacts import mozilla.components.feature.pwa.ProgressiveWebAppFacts +import mozilla.components.feature.top.sites.facts.TopSitesFacts +import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment import mozilla.components.support.base.Component import mozilla.components.support.base.facts.Action import mozilla.components.support.base.facts.Fact @@ -75,6 +77,7 @@ internal class DebugMetricController( } @VisibleForTesting +@Suppress("LargeClass") internal class ReleaseMetricController( private val services: List, private val isDataTelemetryEnabled: () -> Boolean, @@ -134,6 +137,13 @@ internal class ReleaseMetricController( } } + @VisibleForTesting + internal fun factToEvent( + fact: Fact + ): Event? { + return fact.toEvent() + } + private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type) private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) { @@ -242,6 +252,43 @@ internal class ReleaseMetricController( Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> { Event.ProgressiveWebAppInstallAsShortcut } + Component.FEATURE_TOP_SITES to TopSitesFacts.Items.COUNT -> { + value?.let { + var count = 0 + try { + count = it.toInt() + } catch (e: NumberFormatException) { + // Do nothing + } + + return if (count > 0) { + Event.HaveTopSites + } else { + Event.HaveNoTopSites + } + } + null + } + Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT -> { + Event.SecurePrefsExperimentFailure(metadata?.get("javaClass") as String? ?: "null") + } + Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.GET -> { + if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) { + Event.SecurePrefsGetFailure(metadata?.get("javaClass") as String? ?: "null") + } else { + Event.SecurePrefsGetSuccess(value ?: "") + } + } + Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.WRITE -> { + if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) { + Event.SecurePrefsWriteFailure(metadata?.get("javaClass") as String? ?: "null") + } else { + Event.SecurePrefsWriteSuccess + } + } + Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.RESET -> { + Event.SecurePrefsReset + } else -> null } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/SecurePrefsTelemetry.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/SecurePrefsTelemetry.kt new file mode 100644 index 000000000..47a34497a --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/SecurePrefsTelemetry.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.components.metrics + +import android.content.Context +import android.os.Build +import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment +import mozilla.components.service.nimbus.NimbusApi +import org.mozilla.fenix.experiments.ExperimentBranch +import org.mozilla.fenix.experiments.Experiments +import org.mozilla.fenix.ext.withExperiment + +/** + * Allows starting a quick test of ACs SecureAbove22Preferences that will emit Facts + * for the basic operations and allow us to log them for later evaluation of APIs stability. + */ +class SecurePrefsTelemetry( + private val appContext: Context, + private val experiments: NimbusApi +) { + suspend fun startTests() { + // The Android Keystore is used to secure the shared prefs only on API 23+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // These tests should run only if the experiment is live + experiments.withExperiment(Experiments.ANDROID_KEYSTORE) { experimentBranch -> + // .. and this device is not in the control group. + if (experimentBranch == ExperimentBranch.TREATMENT) { + SecurePrefsReliabilityExperiment(appContext)() + } + } + } + } +} 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 69c384865..942390abd 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 @@ -5,11 +5,14 @@ package org.mozilla.fenix.components.toolbar import androidx.navigation.NavController -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab +import mozilla.components.browser.state.selector.getNormalOrPrivateTabs +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.ui.tabcounter.TabCounterMenu import org.mozilla.fenix.HomeActivity @@ -22,7 +25,6 @@ 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.nav -import org.mozilla.fenix.ext.sessionsOfType import org.mozilla.fenix.ext.settings import org.mozilla.fenix.home.HomeScreenViewModel @@ -41,20 +43,20 @@ interface BrowserToolbarController { class DefaultBrowserToolbarController( private val store: BrowserStore, + private val tabsUseCases: TabsUseCases, private val activity: HomeActivity, private val navController: NavController, private val metrics: MetricController, private val readerModeController: ReaderModeController, - private val sessionManager: SessionManager, private val engineView: EngineView, private val homeViewModel: HomeScreenViewModel, - private val customTabSession: Session?, + private val customTabSessionId: String?, private val onTabCounterClicked: () -> Unit, - private val onCloseTab: (Session) -> Unit + private val onCloseTab: (SessionState) -> Unit ) : BrowserToolbarController { private val currentSession - get() = customTabSession ?: sessionManager.selectedSession + get() = store.state.findCustomTabOrSelectedTab(customTabSessionId) override fun handleToolbarPaste(text: String) { navController.nav( @@ -112,18 +114,16 @@ class DefaultBrowserToolbarController( metrics.track( Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB) ) - sessionManager.selectedSession?.let { + store.state.selectedTab?.let { // When closing the last tab we must show the undo snackbar in the home fragment - if (sessionManager.sessionsOfType(it.private).count() == 1) { + if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) { homeViewModel.sessionToDelete = it.id navController.navigate( BrowserFragmentDirections.actionGlobalHome() ) } else { onCloseTab.invoke(it) - // The removeTab use case does not currently select a parent session, so - // we are using sessionManager.remove - sessionManager.remove(it, selectParentIfExists = true) + tabsUseCases.removeTab(it.id, selectParentIfExists = 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 fae8e867a..d72f9b133 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarMenuController.kt @@ -15,9 +15,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import mozilla.appservices.places.BookmarkRoot -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTab +import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.prompt.ShareData @@ -51,19 +52,19 @@ interface BrowserToolbarMenuController { fun handleToolbarItemInteraction(item: ToolbarMenu.Item) } -@Suppress("LargeClass") +@Suppress("LargeClass", "ForbiddenComment") class DefaultBrowserToolbarMenuController( + private val store: BrowserStore, private val activity: HomeActivity, private val navController: NavController, private val metrics: MetricController, private val settings: Settings, private val readerModeController: ReaderModeController, private val sessionFeature: ViewBoundFeatureWrapper, - private val sessionManager: SessionManager, private val findInPageLauncher: () -> Unit, private val browserAnimator: BrowserAnimator, private val swipeRefresh: SwipeRefreshLayout, - private val customTabSession: Session?, + private val customTabSessionId: String?, private val openInFenixIntent: Intent, private val bookmarkTapped: (String, String) -> Unit, private val scope: CoroutineScope, @@ -73,7 +74,7 @@ class DefaultBrowserToolbarMenuController( ) : BrowserToolbarMenuController { private val currentSession - get() = customTabSession ?: sessionManager.selectedSession + get() = store.state.findCustomTabOrSelectedTab(customTabSessionId) // We hold onto a reference of the inner scope so that we can override this with the // TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests @@ -84,29 +85,105 @@ class DefaultBrowserToolbarMenuController( @Suppress("ComplexMethod", "LongMethod") override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) { val sessionUseCases = activity.components.useCases.sessionUseCases + val customTabUseCases = activity.components.useCases.customTabsUseCases trackToolbarItemInteraction(item) Do exhaustive when (item) { + // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 + // todo === Start === + is ToolbarMenu.Item.InstallToHomeScreen -> { + settings.installPwaOpened = true + MainScope().launch { + with(activity.components.useCases.webAppUseCases) { + if (isInstallable()) { + addToHomescreen() + } else { + val directions = + BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment() + navController.navigateSafe(R.id.browserFragment, directions) + } + } + } + } + is ToolbarMenu.Item.OpenInFenix -> { + customTabSessionId?.let { + // Stop the SessionFeature from updating the EngineView and let it release the session + // from the EngineView so that it can immediately be rendered by a different view once + // we switch to the actual browser. + sessionFeature.get()?.release() + + // Turn this Session into a regular tab and then select it + customTabUseCases.migrate(customTabSessionId, select = true) + + // Switch to the actual browser which should now display our new selected session + activity.startActivity(openInFenixIntent.apply { + // We never want to launch the browser in the same task as the external app + // activity. So we force a new task here. IntentReceiverActivity will do the + // right thing and take care of routing to an already existing browser and avoid + // cloning a new one. + flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK + }) + + // Close this activity (and the task) since it is no longer displaying any session + activity.finishAndRemoveTask() + } + } + 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 + // is dismissed. + val snackbar: FenixSnackbar? = activity.getRootView()?.let { v -> + FenixSnackbar.make( + view = v, + duration = Snackbar.LENGTH_LONG, + isDisplayedWithBrowserToolbar = true + ) + .setText(v.context.getString(R.string.deleting_browsing_data_in_progress)) + } + + deleteAndQuit(activity, scope, snackbar) + } + is ToolbarMenu.Item.ReaderModeAppearance -> { + 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( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( - activeSessionId = customTabSession?.id + activeSessionId = customTabSessionId ) ) } else { - sessionUseCases.goBack.invoke(currentSession) + currentSession?.let { + sessionUseCases.goBack.invoke(it.id) + } } } is ToolbarMenu.Item.Forward -> { if (item.viewHistory) { navController.navigate( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( - activeSessionId = customTabSession?.id + activeSessionId = customTabSessionId ) ) } else { - sessionUseCases.goForward.invoke(currentSession) + currentSession?.let { + sessionUseCases.goForward.invoke(it.id) + } } } is ToolbarMenu.Item.Reload -> { @@ -116,24 +193,46 @@ class DefaultBrowserToolbarMenuController( LoadUrlFlags.none() } - sessionUseCases.reload.invoke(currentSession, flags = flags) + currentSession?.let { + sessionUseCases.reload.invoke(it.id, flags = flags) + } + } + is ToolbarMenu.Item.Stop -> { + currentSession?.let { + sessionUseCases.stopLoading.invoke(it.id) + } } - ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession) - ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.Share -> { + val directions = NavGraphDirections.actionGlobalShareFragment( + data = arrayOf( + ShareData( + url = getProperUrl(currentSession), + title = currentSession?.content?.title + ) + ), + showPage = true + ) + navController.navigate(directions) + } + is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() navController.nav(R.id.browserFragment, directions) } - ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment() ) } - is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke( - item.isChecked, - currentSession - ) - ToolbarMenu.Item.AddToTopSites -> { + is ToolbarMenu.Item.RequestDesktop -> { + currentSession?.let { + sessionUseCases.requestDesktopSite.invoke( + item.isChecked, + it.id + ) + } + } + is ToolbarMenu.Item.AddToTopSites -> { scope.launch { val context = swipeRefresh.context val numPinnedSites = @@ -152,7 +251,7 @@ class DefaultBrowserToolbarMenuController( ioScope.launch { currentSession?.let { with(activity.components.useCases.topSitesUseCase) { - addPinnedSites(it.title, it.url) + addPinnedSites(it.content.title, it.content.url) } } }.join() @@ -169,7 +268,7 @@ class DefaultBrowserToolbarMenuController( } } } - ToolbarMenu.Item.AddToHomeScreen, ToolbarMenu.Item.InstallToHomeScreen -> { + is ToolbarMenu.Item.AddToHomeScreen -> { settings.installPwaOpened = true MainScope().launch { with(activity.components.useCases.webAppUseCases) { @@ -183,31 +282,17 @@ class DefaultBrowserToolbarMenuController( } } } - ToolbarMenu.Item.Share -> { - val directions = NavGraphDirections.actionGlobalShareFragment( - data = arrayOf( - ShareData( - url = getProperUrl(currentSession), - title = currentSession?.title - ) - ), - showPage = true - ) - navController.navigate(directions) - } - - ToolbarMenu.Item.FindInPage -> { + is ToolbarMenu.Item.FindInPage -> { findInPageLauncher() metrics.track(Event.FindInPageOpened) } - - ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalAddonsManagementFragment() ) } - ToolbarMenu.Item.SaveToCollection -> { + is ToolbarMenu.Item.SaveToCollection -> { metrics .track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER)) @@ -225,92 +310,45 @@ class DefaultBrowserToolbarMenuController( navController.nav(R.id.browserFragment, directions) } } - ToolbarMenu.Item.OpenInFenix -> { - // Stop the SessionFeature from updating the EngineView and let it release the session - // from the EngineView so that it can immediately be rendered by a different view once - // we switch to the actual browser. - sessionFeature.get()?.release() - - // Strip the CustomTabConfig to turn this Session into a regular tab and then select it - customTabSession!!.customTabConfig = null - sessionManager.select(customTabSession) - - // Switch to the actual browser which should now display our new selected session - activity.startActivity(openInFenixIntent.apply { - // We never want to launch the browser in the same task as the external app - // activity. So we force a new task here. IntentReceiverActivity will do the - // right thing and take care of routing to an already existing browser and avoid - // cloning a new one. - flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK - }) - - // Close this activity (and the task) since it is no longer displaying any session - activity.finishAndRemoveTask() - } - 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 - // is dismissed. - val snackbar: FenixSnackbar? = activity.getRootView()?.let { v -> - FenixSnackbar.make( - view = v, - duration = Snackbar.LENGTH_LONG, - isDisplayedWithBrowserToolbar = true - ) - .setText(v.context.getString(R.string.deleting_browsing_data_in_progress)) - } - - deleteAndQuit(activity, scope, snackbar) - } - ToolbarMenu.Item.ReaderModeAppearance -> { - readerModeController.showControls() - metrics.track(Event.ReaderModeAppearanceOpened) - } - ToolbarMenu.Item.OpenInApp -> { - settings.openInAppOpened = true - - val appLinksUseCases = activity.components.useCases.appLinksUseCases - val getRedirect = appLinksUseCases.appLinkRedirect - currentSession?.let { - val redirect = getRedirect.invoke(it.url) - redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK - appLinksUseCases.openAppLink.invoke(redirect.appIntent) - } - } - ToolbarMenu.Item.Bookmark -> { - sessionManager.selectedSession?.let { - getProperUrl(it)?.let { url -> bookmarkTapped(url, it.title) } + is ToolbarMenu.Item.Bookmark -> { + store.state.selectedTab?.let { + getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) } } } - ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) ) } - ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalHistoryFragment() ) } - ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { + is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { navController.nav( R.id.browserFragment, BrowserFragmentDirections.actionGlobalDownloadsFragment() ) } + is ToolbarMenu.Item.NewTab -> { + navController.navigate( + BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) + ) + } } } - private fun getProperUrl(currentSession: Session?): String? { + private fun getProperUrl(currentSession: SessionState?): String? { return currentSession?.id?.let { val currentTab = browserStore.state.findTab(it) if (currentTab?.readerState?.active == true) { currentTab.readerState.activeUrl } else { - currentSession.url + currentSession.content.url } } } @@ -318,35 +356,38 @@ 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.Quit -> Event.BrowserMenuItemTapped.Item.QUIT + is ToolbarMenu.Item.ReaderModeAppearance -> + 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 - ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP - ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS + is ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP + is ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE + is ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS is ToolbarMenu.Item.RequestDesktop -> if (item.isChecked) { Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON } else { Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF } - - ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE - ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX - ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE - ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION - ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES - ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN - ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS - ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN - ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT - ToolbarMenu.Item.ReaderModeAppearance -> - Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE - ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP - ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK - ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER - ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS - ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY - ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS + is ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE + is ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION + 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.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 } 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 54f91e70f..f50a9595f 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 @@ -9,25 +9,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.annotation.LayoutRes +import androidx.annotation.VisibleForTesting import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat -import androidx.core.view.updateLayoutParams import androidx.lifecycle.LifecycleOwner -import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL -import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP import kotlinx.android.extensions.LayoutContainer -import kotlinx.android.synthetic.main.component_browser_top_toolbar.* -import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.* import kotlinx.coroutines.ExperimentalCoroutinesApi import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider -import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.browser.toolbar.BrowserToolbar -import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior +import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.support.utils.URLStringUtils import mozilla.components.ui.tabcounter.TabCounterMenu @@ -40,6 +33,7 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.utils.ToolbarPopupWindow import java.lang.ref.WeakReference +import mozilla.components.browser.toolbar.behavior.ToolbarPosition as MozacToolbarPosition interface BrowserToolbarViewInteractor { fun onBrowserToolbarPaste(text: String) @@ -58,7 +52,7 @@ class BrowserToolbarView( private val container: ViewGroup, private val toolbarPosition: ToolbarPosition, private val interactor: BrowserToolbarViewInteractor, - private val customTabSession: Session?, + private val customTabSession: CustomTabSessionState?, private val lifecycleOwner: LifecycleOwner ) : LayoutContainer { @@ -76,14 +70,16 @@ class BrowserToolbarView( private val layout = LayoutInflater.from(container.context) .inflate(toolbarLayout, container, true) - val view: BrowserToolbar = layout + @VisibleForTesting + internal var view: BrowserToolbar = layout .findViewById(R.id.toolbar) val toolbarIntegration: ToolbarIntegration - private val isPwaTabOrTwaTab: Boolean - get() = customTabSession?.customTabConfig?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP || - customTabSession?.customTabConfig?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY + @VisibleForTesting + internal val isPwaTabOrTwaTab: Boolean + get() = customTabSession?.config?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP || + customTabSession?.config?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY init { val isCustomTabSession = customTabSession != null @@ -101,17 +97,8 @@ class BrowserToolbarView( with(container.context) { val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported() - if (toolbarPosition == ToolbarPosition.TOP) { - val offsetChangedListener = - AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int -> - interactor.onScrolled(verticalOffset) - } - - app_bar.addOnOffsetChangedListener(offsetChangedListener) - } - view.apply { - setScrollFlags() + setToolbarBehavior() elevation = resources.getDimension(R.dimen.browser_fragment_toolbar_elevation) @@ -203,7 +190,7 @@ class BrowserToolbarView( view, menuToolbar, customTabSession.id, - isPrivate = customTabSession.private + isPrivate = customTabSession.content.private ) } else { DefaultToolbarIntegration( @@ -227,55 +214,76 @@ class BrowserToolbarView( if (isPwaTabOrTwaTab) { return } - when (settings.toolbarPosition) { - ToolbarPosition.BOTTOM -> { - (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - // behavior can be null if the "Scroll to hide toolbar" setting is toggled off. - (behavior as? BrowserToolbarBottomBehavior)?.forceExpand(view) - } - } - ToolbarPosition.TOP -> { - layout.app_bar?.setExpanded(true) - } + + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + (behavior as? BrowserToolbarBehavior)?.forceExpand(view) + } + } + + fun collapse() { + // collapse only for normal tabs and custom tabs not for PWA or TWA. Mirror expand() + if (isPwaTabOrTwaTab) { + return + } + + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + (behavior as? BrowserToolbarBehavior)?.forceCollapse(view) } } + fun dismissMenu() { + view.dismissMenu() + } + /** - * Dynamically sets scroll flags for the toolbar when the user does not have a screen reader enabled - * Note that the toolbar will have the flags set and be able to be hidden - * only if the user didn't disabled this behavior in app's settings. + * Sets whether the toolbar will have a dynamic behavior (to be scrolled) or not. + * + * This will intrinsically check and disable the dynamic behavior if + * - this is disabled in app settings + * - toolbar is placed at the bottom and tab shows a PWA or TWA + * + * Also if the user has not explicitly set a toolbar position and has a screen reader enabled + * the toolbar will be placed at the top and in a fixed position. + * + * @param shouldDisableScroll force disable of the dynamic behavior irrespective of the intrinsic checks. */ - fun setScrollFlags(shouldDisableScroll: Boolean = false) { + fun setToolbarBehavior(shouldDisableScroll: Boolean = false) { when (settings.toolbarPosition) { ToolbarPosition.BOTTOM -> { - if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab) { - (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { - behavior = BrowserToolbarBottomBehavior(view.context, null) - } + if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab && !settings.shouldUseFixedTopToolbar) { + setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } else { - expand() + expandToolbarAndMakeItFixed() } } ToolbarPosition.TOP -> { - view.updateLayoutParams { - scrollFlags = - if (settings.shouldUseFixedTopToolbar || - !settings.isDynamicToolbarEnabled || - shouldDisableScroll) { - // Force expand the toolbar so the user is not stuck with a hidden toolbar - expand() - 0 - } else { - SCROLL_FLAG_SCROLL or - SCROLL_FLAG_ENTER_ALWAYS or - SCROLL_FLAG_SNAP or - SCROLL_FLAG_EXIT_UNTIL_COLLAPSED - } + if (settings.shouldUseFixedTopToolbar || + !settings.isDynamicToolbarEnabled || + shouldDisableScroll + ) { + expandToolbarAndMakeItFixed() + } else { + setDynamicToolbarBehavior(MozacToolbarPosition.TOP) } } } } + @VisibleForTesting + internal fun expandToolbarAndMakeItFixed() { + expand() + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + behavior = null + } + } + + @VisibleForTesting + internal fun setDynamicToolbarBehavior(toolbarPosition: MozacToolbarPosition) { + (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { + behavior = BrowserToolbarBehavior(view.context, null, toolbarPosition) + } + } + @Suppress("ComplexCondition") private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) { if (this is ToolbarMenu.Item.Reload && this.bypassCache || 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 0fc1a3544..10018c227 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 @@ -32,6 +32,7 @@ import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature import mozilla.components.lib.state.ext.flowScoped import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -69,7 +70,12 @@ class DefaultToolbarMenu( override val menuBuilder by lazy { WebExtensionBrowserMenuBuilder( - menuItems, + items = + if (FeatureFlags.toolbarMenuFeature) { + newCoreMenuItems + } else { + oldCoreMenuItems + }, endOfMenuAlwaysVisible = !shouldReverseItems, store = store, webExtIconTintColorResource = primaryTextColor(), @@ -179,20 +185,159 @@ class DefaultToolbarMenu( } ?: false // End of predicates // - private val menuItems by lazy { + private val oldCoreMenuItems by lazy { + val settings = 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 desktopMode = 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 addToTopSites = 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 addToHomescreen = 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 syncedTabs = BrowserMenuImageText( + label = context.getString(R.string.synced_tabs), + imageResource = R.drawable.ic_synced_tabs, + iconTintColorResource = primaryTextColor() + ) { + 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, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.FindInPage) + } + + val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( + id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID + ) + + val saveToCollection = 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 readerAppearance = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_read_appearance), + imageResource = R.drawable.ic_readermode_appearance, + iconTintColorResource = primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance) + } + + 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 historyItem = BrowserMenuImageText( + context.getString(R.string.library_history), + R.drawable.ic_history, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.History) + } + + val bookmarksItem = BrowserMenuImageText( + context.getString(R.string.library_bookmarks), + R.drawable.ic_bookmark_filled, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) + } + + val downloadsItem = BrowserMenuImageText( + context.getString(R.string.library_downloads), + R.drawable.ic_download, + primaryTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Downloads) + } + // Predicates that are called once, during screen init val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity) ?.browsingModeManager?.mode == BrowsingMode.Normal val shouldDeleteDataOnQuit = context.components.settings .shouldDeleteBrowsingDataOnQuit - val syncedTabsInTabsTray = context.components.settings - .syncedTabsInTabsTray val menuItems = listOfNotNull( downloadsItem, historyItem, bookmarksItem, - if (syncedTabsInTabsTray) null else syncedTabs, + syncedTabs, settings, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, BrowserMenuDivider(), @@ -216,151 +361,146 @@ class DefaultToolbarMenu( } } - private val settings = 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) - } - - private val desktopMode = BrowserMenuImageSwitch( - imageResource = R.drawable.ic_desktop, - label = context.getString(R.string.browser_menu_desktop_site), - initialState = { - selectedSession?.content?.desktopMode ?: false + private val newCoreMenuItems by lazy { + val newTabItem = BrowserMenuImageText( + context.getString(R.string.library_new_tab), + R.drawable.ic_bookmark_filled, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.NewTab) } - ) { checked -> - onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) - } - private val addToTopSites = 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 = BrowserMenuImageText( + context.getString(R.string.library_bookmarks), + R.drawable.ic_bookmark_filled, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) + } - private val addToHomescreen = 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 historyItem = BrowserMenuImageText( + context.getString(R.string.library_history), + R.drawable.ic_history, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.History) + } - private val syncedTabs = BrowserMenuImageText( - label = context.getString(R.string.synced_tabs), - imageResource = R.drawable.ic_synced_tabs, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) - } + val downloadsItem = BrowserMenuImageText( + context.getString(R.string.library_downloads), + R.drawable.ic_download, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.Downloads) + } - private 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 + val extensionsItem = BrowserMenuImageText( + context.getString(R.string.browser_menu_extensions), + R.drawable.ic_addons_extensions, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddonsManager) } - ) { - onItemTapped.invoke(ToolbarMenu.Item.InstallToHomeScreen) - } - private val findInPage = 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 syncedTabsItem = BrowserMenuImageText( + context.getString(R.string.library_synced_tabs), + R.drawable.ic_synced_tabs, + disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) + } - private val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( - id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID - ) + val findInPageItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_find_in_page), + imageResource = R.drawable.mozac_ic_search, + iconTintColorResource = disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.FindInPage) + } - private val saveToCollection = 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 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)) + } - private 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 addToHomeScreenItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_add_to_homescreen), + imageResource = R.drawable.ic_add_to_homescreen, + iconTintColorResource = disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) + } - private val readerAppearance = BrowserMenuImageText( - label = context.getString(R.string.browser_menu_read_appearance), - imageResource = R.drawable.ic_readermode_appearance, - iconTintColorResource = primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance) - } + val addToTopSitesItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_add_to_top_sites), + imageResource = R.drawable.ic_top_sites, + iconTintColorResource = disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites) + } - private 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 saveToCollectionItem = BrowserMenuImageText( + label = context.getString(R.string.browser_menu_save_to_collection_2), + imageResource = R.drawable.ic_tab_collection, + iconTintColorResource = disabledTextColor() + ) { + onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) + } - val historyItem = BrowserMenuImageText( - context.getString(R.string.library_history), - R.drawable.ic_history, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.History) - } + val settingsItem = BrowserMenuHighlightableItem( + label = context.getString(R.string.browser_menu_settings), + startImageResource = R.drawable.ic_settings, + iconTintColorResource = disabledTextColor(), + 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 bookmarksItem = BrowserMenuImageText( - context.getString(R.string.library_bookmarks), - R.drawable.ic_bookmark_filled, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) - } + val menuItems = listOfNotNull( + newTabItem, + BrowserMenuDivider(), + bookmarksItem, + historyItem, + downloadsItem, + extensionsItem, + syncedTabsItem, + BrowserMenuDivider(), + findInPageItem, + desktopSiteItem, + BrowserMenuDivider(), + addToHomeScreenItem.apply { visible = ::canAddToHomescreen }, + addToTopSitesItem, + saveToCollectionItem, + BrowserMenuDivider(), + settingsItem, + BrowserMenuDivider(), + menuToolbar + ) - val downloadsItem = BrowserMenuImageText( - context.getString(R.string.library_downloads), - R.drawable.ic_download, - primaryTextColor() - ) { - onItemTapped.invoke(ToolbarMenu.Item.Downloads) + menuItems } @ColorRes @VisibleForTesting internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) + @ColorRes + @VisibleForTesting + internal fun disabledTextColor() = R.color.toolbar_menu_transparent + @VisibleForTesting internal fun registerForIsBookmarkedUpdates() { store.flowScoped(lifecycleOwner) { flow -> diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/SwipeRefreshScrollingViewBehavior.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/SwipeRefreshScrollingViewBehavior.kt deleted file mode 100644 index b69bc0f94..000000000 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/SwipeRefreshScrollingViewBehavior.kt +++ /dev/null @@ -1,50 +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.toolbar - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.google.android.material.appbar.AppBarLayout -import kotlinx.coroutines.ExperimentalCoroutinesApi -import mozilla.components.concept.engine.EngineView -import mozilla.components.concept.engine.EngineView.InputResult.INPUT_RESULT_UNHANDLED -import org.mozilla.fenix.ext.settings - -/** - * ScrollingViewBehavior that will setScrollFlags on BrowserToolbar based on EngineView touch handling - */ -@ExperimentalCoroutinesApi -class SwipeRefreshScrollingViewBehavior( - context: Context, - attrs: AttributeSet?, - private val engineView: EngineView, - private val browserToolbarView: BrowserToolbarView -) : AppBarLayout.ScrollingViewBehavior(context, attrs) { - override fun onStartNestedScroll( - coordinatorLayout: CoordinatorLayout, - child: View, - directTargetChild: View, - target: View, - axes: Int, - type: Int - ): Boolean { - - if (!browserToolbarView.view.context.settings().shouldUseBottomToolbar) { - val shouldDisable = engineView.getInputResult() == INPUT_RESULT_UNHANDLED - browserToolbarView.setScrollFlags(shouldDisable) - } - - return super.onStartNestedScroll( - coordinatorLayout, - child, - directTargetChild, - target, - axes, - type - ) - } -} diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt index 018bf6efb..fca3accc6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt @@ -21,7 +21,6 @@ import mozilla.components.feature.tabs.toolbar.TabCounterToolbarButton import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature import mozilla.components.feature.toolbar.ToolbarFeature import mozilla.components.feature.toolbar.ToolbarPresenter -import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.ktx.android.view.hideKeyboard import org.mozilla.fenix.R @@ -45,7 +44,7 @@ abstract class ToolbarIntegration( store, sessionId, ToolbarFeature.UrlRenderConfiguration( - PublicSuffixList(context), + context.components.publicSuffixList, ThemeManager.resolveAttribute(R.attr.primaryText, context), renderStyle = renderStyle ) 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 27b47c309..87cb80ae3 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 @@ -31,6 +31,7 @@ interface ToolbarMenu { object Bookmarks : Item() object History : Item() object Downloads : Item() + object NewTab : Item() } val menuBuilder: BrowserMenuBuilder diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt index 04e902d51..0d5a6fb0f 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt @@ -6,7 +6,6 @@ package org.mozilla.fenix.customtabs import android.app.Activity import androidx.appcompat.content.res.AppCompatResources.getDrawable -import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar @@ -19,7 +18,6 @@ import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.ext.settings class CustomTabsIntegration( - sessionManager: SessionManager, store: BrowserStore, useCases: CustomTabsUseCases, toolbar: BrowserToolbar, @@ -61,16 +59,6 @@ class CustomTabsIntegration( // If in private mode, override toolbar background to use private color // See #5334 if (isPrivate) { - sessionManager.findSessionById(sessionId)?.apply { - val config = customTabConfig - customTabConfig = config?.copy( - // Don't set toolbar background automatically - toolbarColor = null, - // Force tinting the action button - actionButtonConfig = config.actionButtonConfig?.copy(tint = true) - ) - } - toolbar.background = getDrawable(activity, R.drawable.toolbar_background) } } @@ -94,7 +82,9 @@ class CustomTabsIntegration( menuItemIndex = START_OF_MENU_ITEMS_INDEX, window = activity.window, shareListener = { onItemTapped.invoke(ToolbarMenu.Item.Share) }, - closeListener = { activity.finishAndRemoveTask() } + closeListener = { activity.finishAndRemoveTask() }, + updateToolbarBackground = !isPrivate, + forceActionButtonTinting = isPrivate ) override fun start() = feature.start() diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index 016f220c0..dd6eeda75 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -61,7 +61,6 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler customTabsIntegration.set( feature = CustomTabsIntegration( - sessionManager = requireComponents.core.sessionManager, store = requireComponents.core.store, useCases = requireComponents.useCases.customTabsUseCases, toolbar = toolbar, @@ -139,7 +138,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler customTabSessionId, manifest, WebAppSiteControlsBuilder( - requireComponents.core.sessionManager, + requireComponents.core.store, requireComponents.useCases.sessionUseCases.reload, customTabSessionId, manifest diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt b/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt index f592ea839..1244b7a11 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/WebAppSiteControlsBuilder.kt @@ -7,15 +7,16 @@ package org.mozilla.fenix.customtabs import android.app.Notification import android.content.Context import android.content.Intent -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.state.CustomTabSessionState +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.feature.pwa.feature.SiteControlsBuilder import mozilla.components.feature.session.SessionUseCases import org.mozilla.fenix.R class WebAppSiteControlsBuilder( - private val sessionManager: SessionManager, + private val store: BrowserStore, reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase, private val sessionId: String, private val manifest: WebAppManifest @@ -26,9 +27,9 @@ class WebAppSiteControlsBuilder( override fun buildNotification(context: Context, builder: Notification.Builder) { inner.buildNotification(context, builder) - val isPrivateSession = sessionManager.findSessionById(sessionId)?.private ?: false - - if (!isPrivateSession) { return } + if (store.state.findCustomTab(sessionId)?.content?.private != true) { + return + } builder.setSmallIcon(R.drawable.ic_private_browsing) builder.setContentTitle(context.getString(R.string.pwa_site_controls_title_private, manifest.name)) 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 5b5f67b34..9a9a39036 100644 --- a/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt +++ b/app/src/main/java/org/mozilla/fenix/experiments/Experiments.kt @@ -6,8 +6,9 @@ package org.mozilla.fenix.experiments class Experiments { companion object { - const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation" + 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" } } @@ -15,7 +16,7 @@ class ExperimentBranch { companion object { const val TREATMENT = "treatment" const val CONTROL = "control" - const val A1 = "A1" - const val A2 = "A2" + const val A1 = "a1" + const val A2 = "a2" } } diff --git a/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.kt new file mode 100644 index 000000000..1622e79da --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/experiments/NimbusSetup.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.experiments + +import android.content.Context +import android.net.Uri +import android.os.StrictMode +import io.sentry.Sentry +import mozilla.components.service.nimbus.NimbusApi +import mozilla.components.service.nimbus.Nimbus +import mozilla.components.service.nimbus.NimbusDisabled +import mozilla.components.service.nimbus.NimbusServerSettings +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.components.isSentryEnabled +import org.mozilla.fenix.ext.components +import org.mozilla.fenix.ext.settings + +@Suppress("TooGenericExceptionCaught") +fun createNimbus(context: Context, url: String?): NimbusApi = + try { + // Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT. + // but we keep this here to not mix feature flags and how we configure Nimbus. + val serverSettings = if (!url.isNullOrBlank()) { + NimbusServerSettings(url = Uri.parse(url)) + } else { + null + } + + // Global opt out state is stored in Nimbus, and shouldn't be toggled to `true` + // from the app unless the user does so from a UI control. + // However, the user may have opt-ed out of mako experiments already, so + // we should respect that setting here. + val enabled = + context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { + context.settings().isExperimentationEnabled + } + + Nimbus(context, serverSettings).apply { + // This performs the minimal amount of work required to load branch and enrolment data + // into memory. If `getExperimentBranch` is called from another thread between here + // and the next nimbus disk write (setting `globalUserParticipation` or + // `applyPendingExperiments()`) then this has you covered. + // This call does its work on the db thread. + initialize() + + if (!enabled) { + // This opts out of nimbus experiments. It involves writing to disk, so does its + // work on the db thread. + globalUserParticipation = enabled + } + + // We may have downloaded experiments on a previous run, so let's start using them + // now. We didn't do this earlier, so as to make getExperimentBranch and friends returns + // the same thing throughout the session. This call does its work on the db thread. + applyPendingExperiments() + + // Now fetch the experiments from the server. These will be available for feature + // configuration on the next run of the app. This call launches on the fetch thread. + fetchExperiments() + } + } catch (e: Throwable) { + // Something went wrong. We'd like not to, but stability of the app is more important than + // failing fast here. + if (isSentryEnabled()) { + Sentry.capture(e) + } else { + Logger.error("Failed to initialize Nimbus", e) + } + NimbusDisabled() + } diff --git a/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt b/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt index 2152f3fb8..1d6f5388f 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/BookmarkNode.kt @@ -15,6 +15,6 @@ val Context.bookmarkStorage: PlacesBookmarksStorage * Removes [children] from [BookmarkNode.children] and returns the new modified [BookmarkNode]. */ operator fun BookmarkNode.minus(children: Set): BookmarkNode { - val removedChildrenGuids = children.map { it.guid }.toSet() + val removedChildrenGuids = children.map { it.guid } return this.copy(children = this.children?.filterNot { removedChildrenGuids.contains(it.guid) }) } diff --git a/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt b/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt deleted file mode 100644 index 3339b01b1..000000000 --- a/app/src/main/java/org/mozilla/fenix/ext/BrowserState.kt +++ /dev/null @@ -1,16 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.ext - -import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.MediaState - -fun BrowserState.getMediaStateForSession(sessionId: String): MediaState.State { - return if (media.aggregate.activeTabId == sessionId) { - media.aggregate.state - } else { - MediaState.State.NONE - } -} 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 49cf69cd5..52d7389bc 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -7,11 +7,11 @@ package org.mozilla.fenix.home import android.animation.Animator import android.content.Context import android.content.DialogInterface +import android.content.res.Configuration import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.os.StrictMode -import android.view.Display.FLAG_SECURE import android.view.Gravity import android.view.LayoutInflater import android.view.View @@ -84,6 +84,7 @@ import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.ui.tabcounter.TabCounterMenu import org.mozilla.fenix.BrowserDirection +import org.mozilla.fenix.Config import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions @@ -103,8 +104,9 @@ import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.runIfFragmentIsAttached +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlView @@ -166,6 +168,9 @@ class HomeFragment : Fragment() { private val topSitesFeature = ViewBoundFeatureWrapper() + @VisibleForTesting + internal var getMenuButton: () -> MenuButton? = { menuButton } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -175,6 +180,12 @@ class HomeFragment : Fragment() { requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun) } } + + if (!onboarding.userHasBeenOnboarded() && + requireContext().settings().shouldShowPrivacyPopWindow && + Config.channel.isMozillaOnline) { + showPrivacyPopWindow(requireContext(), requireActivity()) + } } @Suppress("LongMethod") @@ -223,7 +234,7 @@ class HomeFragment : Fragment() { storage = components.core.topSitesStorage, config = ::getTopSitesConfig ), - owner = this, + owner = viewLifecycleOwner, view = view ) @@ -266,6 +277,12 @@ class HomeFragment : Fragment() { return view } + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + getMenuButton()?.dismissMenu() + } + private fun dismissTip(tip: Tip) { sessionControlInteractor.onCloseTip(tip) } @@ -389,12 +406,6 @@ class HomeFragment : Fragment() { } } - if (browsingModeManager.mode.isPrivate) { - requireActivity().window.addFlags(FLAG_SECURE) - } else { - requireActivity().window.clearFlags(FLAG_SECURE) - } - consumeFrom(requireComponents.core.store) { updateTabCounter(it) } @@ -533,7 +544,6 @@ class HomeFragment : Fragment() { sessionControlView = null appBarLayout = null bundleArgs.clear() - requireActivity().window.clearFlags(FLAG_SECURE) } override fun onStart() { 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 dc6b78d7d..f1284ee6a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragmentStore.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.home import android.graphics.Bitmap -import mozilla.components.browser.state.state.MediaState import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.top.sites.TopSite import mozilla.components.lib.state.Action @@ -28,8 +27,7 @@ data class Tab( val hostname: String, val title: String, val selected: Boolean? = null, - val icon: Bitmap? = null, - val mediaState: MediaState.State + val icon: Bitmap? = null ) /** 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 027e6cb26..6c1a32760 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeMenu.kt @@ -190,7 +190,7 @@ class HomeMenu( if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null, settingsItem, BrowserMenuDivider(), - if (settings.syncedTabsInTabsTray) null else syncedTabsItem, + syncedTabsItem, bookmarksItem, historyItem, downloadsItem, diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt index dec4cac01..5d59643ae 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt @@ -7,10 +7,8 @@ package org.mozilla.fenix.home.intent import android.content.Intent import androidx.navigation.NavController import mozilla.components.browser.state.selector.findTab -import mozilla.components.feature.media.service.AbstractMediaService import mozilla.components.feature.media.service.AbstractMediaSessionService import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.ext.components @@ -44,17 +42,9 @@ class OpenSpecificTabIntentProcessor( } private fun getAction(): String { - return if (newMediaSessionApi) { - AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB - } else { - AbstractMediaService.Companion.ACTION_SWITCH_TAB - } + return AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB } private fun getTabId(): String { - return if (newMediaSessionApi) { - AbstractMediaSessionService.Companion.EXTRA_TAB_ID - } else { - AbstractMediaService.Companion.EXTRA_TAB_ID - } + return AbstractMediaSessionService.Companion.EXTRA_TAB_ID } diff --git a/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayActivity.kt b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayActivity.kt new file mode 100644 index 000000000..ad4d119cb --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayActivity.kt @@ -0,0 +1,84 @@ +/* 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.mozonline + +import android.app.Activity +import android.content.Context +import android.os.Bundle +import android.util.AttributeSet +import android.view.View +import android.widget.ImageButton +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate +import mozilla.components.feature.search.BrowserStoreSearchAdapter +import mozilla.components.support.ktx.android.content.call +import mozilla.components.support.ktx.android.content.email +import mozilla.components.support.ktx.android.content.share +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.components + +/** + * A special activity for displaying the detail content about privacy hyperlinked in alert dialog. + */ + +class PrivacyContentDisplayActivity : Activity(), EngineSession.Observer { + private lateinit var engineView: EngineView + private lateinit var closeButton: ImageButton + private lateinit var engineSession: EngineSession + private var url: String? = "" + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_privacy_content_display) + val addr = intent.extras + if (addr != null) { + url = addr.getString("url") + } + + engineView = findViewById(R.id.privacyContentEngineView) as EngineView + closeButton = findViewById(R.id.privacyContentCloseButton) as ImageButton + engineSession = components.core.engine.createSession() + } + + override fun onCreateView( + parent: View?, + name: String, + context: Context, + attrs: AttributeSet + ): View? = when (name) { + EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply { + selectionActionDelegate = DefaultSelectionActionDelegate( + BrowserStoreSearchAdapter( + components.core.store + ), + resources = context.resources, + shareTextClicked = { share(it) }, + emailTextClicked = { email(it) }, + callTextClicked = { call(it) } + ) + }.asView() + else -> super.onCreateView(parent, name, context, attrs) + } + + override fun onStart() { + super.onStart() + engineSession.register(this) + engineSession.let { engineSession -> + engineView.render(engineSession) + url?.let { engineSession.loadUrl(it) } + } + closeButton.setOnClickListener { finish() } + } + + override fun onStop() { + super.onStop() + engineSession.unregister(this) + } + + override fun onDestroy() { + super.onDestroy() + engineSession.close() + } +} diff --git a/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt new file mode 100644 index 000000000..d34371a00 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentDisplayHelper.kt @@ -0,0 +1,53 @@ +/* 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.mozonline + +import android.app.Activity +import android.content.Context +import android.content.DialogInterface +import android.text.SpannableString +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.settings +import kotlin.system.exitProcess + +fun showPrivacyPopWindow(context: Context, activity: Activity) { + val content = context.getString(R.string.privacy_notice_content) + + // Use hyperlinks to display details about privacy + val messageClickable1 = context.getString(R.string.privacy_notice_clickable1) + val messageClickable2 = context.getString(R.string.privacy_notice_clickable2) + val messageClickable3 = context.getString(R.string.privacy_notice_clickable3) + val messageSpannable = SpannableString(content) + + val clickableSpan1 = PrivacyContentSpan(Position.POS1, context) + val clickableSpan2 = PrivacyContentSpan(Position.POS2, context) + val clickableSpan3 = PrivacyContentSpan(Position.POS3, context) + + messageSpannable.setSpan(clickableSpan1, content.indexOf(messageClickable1), + content.indexOf(messageClickable1) + messageClickable1.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + messageSpannable.setSpan(clickableSpan2, content.indexOf(messageClickable2), + content.indexOf(messageClickable2) + messageClickable2.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + messageSpannable.setSpan(clickableSpan3, content.indexOf(messageClickable3), + content.indexOf(messageClickable3) + messageClickable3.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE) + + // Users can only use fenix after they agree with the privacy notice + val builder = AlertDialog.Builder(activity) + .setPositiveButton(context.getString(R.string.privacy_notice_positive_button), + DialogInterface.OnClickListener { _, _ -> + context.settings().shouldShowPrivacyPopWindow = false + }) + .setNeutralButton(context.getString(R.string.privacy_notice_neutral_button), + DialogInterface.OnClickListener { _, _ -> exitProcess(0) }) + .setTitle(context.getString(R.string.privacy_notice_title)) + .setMessage(messageSpannable) + .setCancelable(false) + val alertDialog: AlertDialog = builder.create() + alertDialog.show() + alertDialog.findViewById(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance() +} diff --git a/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentSpan.kt b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentSpan.kt new file mode 100644 index 000000000..c718a6654 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/home/mozonline/PrivacyContentSpan.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.home.mozonline + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.text.style.ClickableSpan +import android.view.View + +object Position { + const val POS1 = 1 + const val POS2 = 2 + const val POS3 = 3 +} + +object ADDR { + const val URL1 = "https://www.mozilla.org/en-US/MPL/" + const val URL2 = "https://www.mozilla.org/en-US/foundation/trademarks/policy/" + const val URL3 = "https://www.mozilla.org/zh-CN/privacy/firefox/" +} + +class PrivacyContentSpan(var pos: Int, var context: Context) : + ClickableSpan() { + override fun onClick(widget: View) { + /** + * To avoid users directly using fenix by clicking these urls before + * they click positive button of privacy notice alert dialog, start + * PrivacyContentDisplayActivity to display them. + */ + val engineViewIntent = Intent(context, PrivacyContentDisplayActivity::class.java) + engineViewIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + val addr = Bundle() + when (pos) { + Position.POS1 -> addr.putString("url", ADDR.URL1) + Position.POS2 -> addr.putString("url", ADDR.URL2) + Position.POS3 -> addr.putString("url", ADDR.URL3) + } + engineViewIntent.putExtras(addr) + context.startActivity(engineViewIntent) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt index f6f6a368d..e85ded68a 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapter.kt @@ -21,23 +21,25 @@ import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHold class BookmarkAdapter(private val emptyView: View, private val interactor: BookmarkViewInteractor) : RecyclerView.Adapter() { - private var tree: List = listOf() + @VisibleForTesting var tree: List = listOf() private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal() private var isFirstRun = true fun updateData(tree: BookmarkNode?, mode: BookmarkFragmentState.Mode) { - // Display folders above all other bookmarks. val allNodes = tree?.children.orEmpty() val folders: MutableList = mutableListOf() val notFolders: MutableList = mutableListOf() + val separators: MutableList = mutableListOf() allNodes.forEach { - if (it.type == BookmarkNodeType.FOLDER) { - folders.add(it) - } else { - notFolders.add(it) + when (it.type) { + BookmarkNodeType.SEPARATOR -> separators.add(it) + BookmarkNodeType.FOLDER -> folders.add(it) + else -> notFolders.add(it) } } - val newTree = folders + notFolders + // Display folders above all other bookmarks. Exclude separators. + // For separator removal, see discussion in https://github.com/mozilla-mobile/fenix/issues/15214 + val newTree = folders + notFolders - separators val diffUtil = DiffUtil.calculateDiff( BookmarkDiffUtil( diff --git a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt index d05b3bbc4..f9687e8ad 100644 --- a/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/downloads/DownloadFragment.kt @@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.BrowserState import kotlinx.coroutines.launch import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.feature.downloads.AbstractFetchDownloadService +import mozilla.components.feature.downloads.DownloadsUseCases import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import org.mozilla.fenix.HomeActivity @@ -50,6 +51,7 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan private lateinit var metrics: MetricController private var undoScope: CoroutineScope? = null private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null + private lateinit var downloadsUseCases: DownloadsUseCases override fun onCreateView( inflater: LayoutInflater, @@ -59,6 +61,7 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan val view = inflater.inflate(R.layout.fragment_downloads, container, false) val items = provideDownloads(requireComponents.core.store.state) + downloadsUseCases = requireContext().components.useCases.downloadUseCases downloadStore = StoreProvider.get(this) { DownloadFragmentStore( @@ -85,6 +88,10 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan return view } + /** + * Returns a list of available downloads to be displayed to the user. + * Downloads must be COMPLETED and existent on disk. + */ @VisibleForTesting internal fun provideDownloads(state: BrowserState): List { return state.downloads.values @@ -128,9 +135,7 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ -> // Use fragment's lifecycle; the view may be gone by the time dialog is interacted with. lifecycleScope.launch(IO) { - context.let { - it.components.useCases.downloadUseCases.removeAllDownloads() - } + downloadsUseCases.removeAllDownloads() updatePendingDownloadToDelete(downloadStore.state.items.toSet()) launch(Dispatchers.Main) { showSnackBar( @@ -146,6 +151,11 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan } } + /** + * Schedules [items] for deletion. + * Note: When tapping on a download item's "trash" button + * (itemView.overflow_menu) this [items].size() will be 1. + */ private fun deleteDownloadItems(items: Set) { metrics.track(Event.DownloadsItemDeleted) @@ -155,10 +165,10 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan requireView(), getMultiSelectSnackBarMessage(items), getString(R.string.bookmark_undo_deletion), - { + onCancel = { undoPendingDeletion(items) }, - getDeleteDownloadItemsOperation(items) + operation = getDeleteDownloadItemsOperation(downloadsUseCases, items) ) } @@ -210,6 +220,9 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan else -> super.onOptionsItemSelected(item) } + /** + * Provides a message to the Undo snackbar. + */ private fun getMultiSelectSnackBarMessage(downloadItems: Set): String { return if (downloadItems.size > 1) { getString(R.string.download_delete_multiple_items_snackbar_1) @@ -246,14 +259,18 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan metrics.track(Event.DownloadsItemOpened) } - private fun getDeleteDownloadItemsOperation(items: Set): (suspend () -> Unit) { + /** + * Launches the coroutine to delete the provided [items]. + */ + private fun getDeleteDownloadItemsOperation( + downloadUseCases: DownloadsUseCases, + items: Set + ): (suspend () -> Unit) { return { CoroutineScope(IO).launch { downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode) - context?.let { - for (item in items) { - it.components.useCases.downloadUseCases.removeDownload(item.id) - } + for (item in items) { + downloadUseCases.removeDownload(item.id) } downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode) pendingDownloadDeletionJob = null @@ -261,8 +278,14 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan } } + /** + * Queues the [getDeleteDownloadItemsOperation] job in [pendingDownloadDeletionJob] in case + * the user exits the fragment and we need to quickly execute the queued deletion. + * And adds the [items] to be deleted to the list of [DownloadFragmentStore.pendingDeletionIds], + * which is used to determine what items to show and what items to hide from the user. + */ private fun updatePendingDownloadToDelete(items: Set) { - pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(items) + pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(downloadsUseCases, items) val ids = items.map { item -> item.id }.toSet() downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids)) } @@ -273,6 +296,9 @@ class DownloadFragment : LibraryPageFragment(), UserInteractionHan downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids)) } + /** + * Executes pending job(s) when leaving [DownloadFragment]. + */ private fun invokePendingDeletion() { pendingDownloadDeletionJob?.let { viewLifecycleOwner.lifecycleScope.launch { diff --git a/app/src/main/java/org/mozilla/fenix/media/MediaService.kt b/app/src/main/java/org/mozilla/fenix/media/MediaService.kt deleted file mode 100644 index 32a1ce2da..000000000 --- a/app/src/main/java/org/mozilla/fenix/media/MediaService.kt +++ /dev/null @@ -1,16 +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.media - -import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.feature.media.service.AbstractMediaService -import org.mozilla.fenix.ext.components - -/** - * [AbstractMediaService] implementation for injecting [BrowserStore] singleton. - */ -class MediaService : AbstractMediaService() { - override val store: BrowserStore by lazy { components.core.store } -} diff --git a/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessor.kt b/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessor.kt new file mode 100644 index 000000000..2e493cb2b --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessor.kt @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.perf + +import android.os.Handler +import android.os.Looper +import androidx.annotation.VisibleForTesting +import androidx.annotation.VisibleForTesting.PRIVATE +import mozilla.components.concept.base.profiler.Profiler +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import mozilla.components.support.base.facts.FactProcessor + +/** + * A fact processor that adds Gecko profiler markers for [Fact]s matching a specific format. + * We look for the following format: + * ``` + * Fact( + * action = Action.IMPLEMENTATION_DETAIL + * item = + * ) + * ``` + * + * This allows us to add profiler markers from android-components code. Using the Fact API for this + * purpose, rather than calling [Profiler.addMarker] directly inside components, has trade-offs. Its + * downsides are that it is less explicit and tooling does not work as well on it. However, we felt + * it was worthwhile because: + * + * 1. we don't know what profiler markers are useful so we want to be able to iterate quickly. + * Adding dependencies on the Profiler and landing these changes across two repos hinders that + * 2. we want to instrument the code as close to specific method calls as possible (e.g. + * GeckoSession.loadUrl) but it's not always easy to do so (e.g. in the previous example, passing a + * Profiler reference to GeckoEngineSession is difficult because GES is not a global dependency) + * 3. we can only add Profiler markers from the main thread so adding markers will become more + * difficult if we have to understand the threading needs of each Profiler call site + * + * An additional benefit with having this infrastructure is that it's easy to add Profiler markers + * for local debugging. + * + * That being said, if we find a location where it would be valuable to have a long term Profiler + * marker, we should consider instrumenting it via the [Profiler] API. + */ +class ProfilerMarkerFactProcessor @VisibleForTesting(otherwise = PRIVATE) constructor( + // We use a provider to defer accessing the profiler until we need it, because the property is a + // child of the engine property and we don't want to initialize it earlier than we intend to. + private val profilerProvider: () -> Profiler?, + private val mainHandler: Handler = Handler(Looper.getMainLooper()), + private val getMyLooper: () -> Looper? = { Looper.myLooper() } +) : FactProcessor { + + override fun process(fact: Fact) { + if (fact.action != Action.IMPLEMENTATION_DETAIL) { + return + } + + val markerName = fact.item + + // Java profiler markers can only be added from the main thread so, for now, we push all + // markers to the the main thread (which also groups all the markers together, + // making it easier to read). + val profiler = profilerProvider() + if (getMyLooper() == mainHandler.looper) { + profiler?.addMarker(markerName) + } else { + // To reduce the performance burden, we could early return if the profiler isn't active. + // However, this would change the performance characteristics from when the profiler is + // active and when it's inactive so we always post instead. + val now = profiler?.getProfilerTime() + mainHandler.post { + // We set now to both start and end time because we want a marker of without duration + // and if end is omitted, the duration is created implicitly. + profiler?.addMarker(markerName, now, now, null) + } + } + } + + companion object { + fun create(profilerProvider: () -> Profiler?) = ProfilerMarkerFactProcessor(profilerProvider) + } +} diff --git a/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkers.kt b/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkers.kt new file mode 100644 index 000000000..783992303 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/ProfilerMarkers.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.perf + +import android.view.View +import androidx.core.view.doOnPreDraw +import mozilla.components.concept.base.profiler.Profiler + +/** + * A container for functions for when adding a profiler marker is less readable + * (e.g. multiple lines, more advanced logic). + */ +object ProfilerMarkers { + + fun homeActivityOnStart(rootContainer: View, profiler: Profiler?) { + rootContainer.doOnPreDraw { + profiler?.addMarker("onPreDraw", "expected first frame via HomeActivity.onStart") + } + } +} 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 dbc4d2c24..47953d9b7 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurement.kt @@ -54,20 +54,30 @@ internal class StartupFrameworkStartMeasurement( if (applicationInitNanos < 0) { telemetry.frameworkStartError.set(true) } else { + val clockTicksPerSecond = stat.clockTicksPerSecond.also { + // framework* is derived from the number of clock ticks per second. To ensure this + // value does not throw off our result, we capture it too. + telemetry.clockTicksPerSecond.add(it.toInt()) + } + + // In our brief analysis, clock ticks per second was overwhelmingly equal to 100. To make + // analysis easier in GLAM, we split the results into two separate metrics. See the + // metric descriptions for more details. + @Suppress("MagicNumber") // it's more confusing to separate the comment above from the value declaration. + val durationMetric = + if (clockTicksPerSecond == 100L) telemetry.frameworkPrimary else telemetry.frameworkSecondary + try { - telemetry.frameworkStart.setRawNanos(getFrameworkStartNanos()) + durationMetric.setRawNanos(getFrameworkStartNanos()) } catch (e: FileNotFoundException) { // Privacy managers can add hooks that block access to reading system /proc files. // We want to catch these exception and report an error on accessing the file // rather than an implementation error. telemetry.frameworkStartReadError.set(true) } - - // frameworkStart is derived from the number of clock ticks per second. To ensure this - // value does not throw off our result, we capture it too. - telemetry.clockTicksPerSecond.add(stat.clockTicksPerSecond.toInt()) } } + /** * @throws [java.io.FileNotFoundException] */ 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 391051707..845076d8c 100644 --- a/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt +++ b/app/src/main/java/org/mozilla/fenix/perf/StrictModeManager.kt @@ -10,6 +10,7 @@ package org.mozilla.fenix.perf import android.os.Build +import android.os.Handler import android.os.Looper import android.os.StrictMode import androidx.annotation.VisibleForTesting @@ -19,11 +20,12 @@ import androidx.fragment.app.FragmentManager import mozilla.components.support.ktx.android.os.resetAfter import org.mozilla.fenix.Config import org.mozilla.fenix.components.Components +import org.mozilla.fenix.utils.ManufacturerCodes import org.mozilla.fenix.utils.Mockable +import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicLong -private const val MANUFACTURE_HUAWEI: String = "HUAWEI" -private const val MANUFACTURE_ONE_PLUS: String = "OnePlus" +private const val DELAY_TO_REMOVE_STRICT_MODE_MILLIS = 1000L private val logger = Performance.logger private val mainLooper = Looper.getMainLooper() @@ -65,8 +67,8 @@ class StrictModeManager( val threadPolicy = StrictMode.ThreadPolicy.Builder() .detectAll() .penaltyLog() - if (setPenaltyDeath && Build.MANUFACTURER !in strictModeExceptionList) { - threadPolicy.penaltyDeath() + if (setPenaltyDeath) { + threadPolicy.penaltyDeathWithIgnores() } StrictMode.setThreadPolicy(threadPolicy.build()) @@ -96,10 +98,18 @@ class StrictModeManager( fragmentManager.registerFragmentLifecycleCallbacks(object : FragmentManager.FragmentLifecycleCallbacks() { override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { - enableStrictMode(setPenaltyDeath = false) fm.unregisterFragmentLifecycleCallbacks(this) - } - }, false) + + // 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) } /** @@ -137,13 +147,35 @@ class StrictModeManager( functionBlock() } } +} - /** - * There are certain manufacturers that have custom font classes for the OS systems. - * These classes violates the [StrictMode] policies on startup. As a workaround, we create - * an exception list for these manufacturers so that dialogs do not show up on start up. - * To add a new manufacturer to the list, log "Build.MANUFACTURER" from the device to get the - * exact name of the manufacturer. - */ - private val strictModeExceptionList = setOf(MANUFACTURE_HUAWEI, MANUFACTURE_ONE_PLUS) +/** + * There are certain manufacturers that have custom font classes for the OS systems. + * These classes violates the [StrictMode] policies on startup. As a workaround, we create + * an exception list for these manufacturers so that dialogs do not show up on start up. + * To add a new manufacturer to the list, log "Build.MANUFACTURER" from the device to get the + * exact name of the manufacturer. + */ +private val strictModeExceptionList = setOf(ManufacturerCodes.HUAWEI, ManufacturerCodes.ONE_PLUS) + +private fun StrictMode.ThreadPolicy.Builder.penaltyDeathWithIgnores(): StrictMode.ThreadPolicy.Builder { + // This workaround was added before we realized we can ignored based on violation contents + // (see code below). This solution - blanket disabling StrictMode on some manufacturers - isn't + // great so, if we have time, we should consider reimplementing these fixes using the methods below. + if (Build.MANUFACTURER in strictModeExceptionList) { + return this + } + + // If we want to apply ignores based on stack trace contents to APIs below P, we can use this methodology: + // https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + penaltyDeath() + } else { + // Ideally, we'd use a shared thread pool but we don't have any on the system currently + // (all shared ones are coroutine dispatchers). + val executor = Executors.newSingleThreadExecutor() + penaltyListener(executor, ThreadPenaltyDeathWithIgnoresListener()) + } + + return this } diff --git a/app/src/main/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListener.kt b/app/src/main/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListener.kt new file mode 100644 index 000000000..fcc7774b3 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListener.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.perf + +import android.os.Build +import android.os.StrictMode +import android.os.strictmode.Violation +import androidx.annotation.RequiresApi +import mozilla.components.support.base.log.logger.Logger +import org.mozilla.fenix.utils.ManufacturerCodes + +private const val FCQN_EDM_STORAGE_PROVIDER_BASE = "com.android.server.enterprise.storage.EdmStorageProviderBase" + +/** + * A [StrictMode.OnThreadViolationListener] that recreates + * [StrictMode.ThreadPolicy.Builder.penaltyDeath] but will ignore some violations. For example, + * sometimes OEMs will add code that violates StrictMode so we can ignore them here instead of + * cluttering up our code with resetAfter. + * + * This class can only be used with Android P+ so we'd have to implement workarounds if the + * violations we want to ignore affect older devices. + */ +@RequiresApi(Build.VERSION_CODES.P) +class ThreadPenaltyDeathWithIgnoresListener( + private val logger: Logger = Performance.logger +) : StrictMode.OnThreadViolationListener { + + override fun onThreadViolation(violation: Violation?) { + if (violation == null) return + + // Unfortunately, this method gets called many (~5+) times with the same violation so we end + // up logging/throwing redundantly. + if (shouldViolationBeIgnored(violation)) { + logger.debug("Ignoring StrictMode ThreadPolicy violation", violation) + } else { + penaltyDeath(violation) + } + } + + @Suppress("TooGenericExceptionThrown") // we throw what StrictMode's penaltyDeath throws. + private fun penaltyDeath(violation: Violation) { + throw RuntimeException("StrictMode ThreadPolicy violation", violation) + } + + private fun shouldViolationBeIgnored(violation: Violation): Boolean = + isSamsungLgEdmStorageProviderStartupViolation(violation) + + private fun isSamsungLgEdmStorageProviderStartupViolation(violation: Violation): Boolean { + // Root issue: https://github.com/mozilla-mobile/fenix/issues/17920 + // + // This fix may address the issues seen in this bug: + // https://github.com/mozilla-mobile/fenix/issues/15430 + // So we might be able to back out the changes made there. However, I don't have a device to + // test so I didn't bother. + // + // This issue occurs on the Galaxy S10e, Galaxy A50, Note 10, and LG G7 FIT but not the S7: + // I'm guessing it's just a problem on recent Samsungs and LGs so it's okay being in this P+ + // listener. + if (!ManufacturerCodes.isSamsung && !ManufacturerCodes.isLG) { + return false + } + + // To ignore this warning, we can inspect the stack trace. There are no parts of the + // violation stack trace that are clearly unique to this violation but + // EdmStorageProviderBase doesn't appear in Android code search so we match against it. + // This class may be used in other violations that we're capable of fixing but this + // code may ignore them. I think it's okay - we keep this code simple and if it was a serious + // issue, we'd catch it on other manufacturers. + return violation.stackTrace.any { it.className == FCQN_EDM_STORAGE_PROVIDER_BASE } + } +} 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 6e20a2972..f1b5adcdb 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -116,6 +116,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { } } + @SuppressWarnings("LongMethod") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -172,10 +173,13 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { val awesomeBar = view.awesome_bar awesomeBar.customizeForBottomToolbar = requireContext().settings().shouldUseBottomToolbar + val fromHomeFragment = + findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment awesomeBarView = AwesomeBarView( activity, interactor, - awesomeBar + awesomeBar, + fromHomeFragment ) view.awesome_bar.setOnTouchListener { _, _ -> @@ -191,7 +195,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { requireComponents.core.engine.speculativeCreateSession(isPrivate) - if (findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment) { + if (fromHomeFragment) { // When displayed above home, dispatches the touch events to scrim area to the HomeFragment view.search_wrapper.background = ColorDrawable(Color.TRANSPARENT) dialog?.window?.decorView?.setOnTouchListener { _, event -> @@ -467,8 +471,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { clear(pill_wrapper.id, BOTTOM) connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP) + clear(awesome_bar.id, TOP) + clear(awesome_bar.id, BOTTOM) + connect(awesome_bar.id, TOP, search_suggestions_hint.id, BOTTOM) + connect(awesome_bar.id, BOTTOM, pill_wrapper.id, TOP) + clear(search_suggestions_hint.id, TOP) + clear(search_suggestions_hint.id, BOTTOM) connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP) + connect(search_suggestions_hint.id, BOTTOM, search_hint_bottom_barrier.id, TOP) clear(fill_link_from_clipboard.id, TOP) connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP) @@ -483,7 +494,10 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler { private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) { view?.apply { - val showHint = state.showSearchSuggestionsHint && !state.showSearchShortcuts + val showHint = state.showSearchSuggestionsHint && + !state.showSearchShortcuts && + state.url != state.query + findViewById(R.id.search_suggestions_hint)?.isVisible = showHint search_suggestions_hint_divider?.isVisible = showHint } 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 7d97dea8f..cd9fa8c8b 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 @@ -10,7 +10,6 @@ import androidx.core.graphics.BlendModeCompat.SRC_IN import androidx.core.graphics.drawable.toBitmap import mozilla.components.browser.awesomebar.BrowserAwesomeBar import mozilla.components.browser.search.DefaultSearchEngineProvider -import mozilla.components.browser.session.Session import mozilla.components.browser.state.search.SearchEngine import mozilla.components.concept.awesomebar.AwesomeBar import mozilla.components.concept.engine.EngineSession @@ -42,7 +41,8 @@ import mozilla.components.browser.search.SearchEngine as LegacySearchEngine class AwesomeBarView( private val activity: HomeActivity, val interactor: AwesomeBarInteractor, - val view: BrowserAwesomeBar + val view: BrowserAwesomeBar, + private val fromHomeFragment: Boolean ) { private val sessionProvider: SessionSuggestionProvider private val historyStorageProvider: HistoryStorageSuggestionProvider @@ -85,10 +85,6 @@ class AwesomeBarView( } private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase { - override fun invoke(session: Session) { - interactor.onExistingSessionSelected(session.id) - } - override fun invoke(tabId: String) { interactor.onExistingSessionSelected(tabId) } @@ -111,7 +107,7 @@ class AwesomeBarView( selectTabUseCase, components.core.icons, getDrawable(activity, R.drawable.ic_search_results_tab), - excludeSelectedSession = true + excludeSelectedSession = !fromHomeFragment ) historyStorageProvider = 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 03f7d5deb..ca0229a2a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/DataChoicesFragment.kt @@ -31,6 +31,10 @@ class DataChoicesFragment : PreferenceFragmentCompat() { } else { context.components.analytics.metrics.stop(MetricServiceType.Data) } + // Reset experiment identifiers on both opt-in and opt-out; it's likely + // that in future we will need to pass in the new telemetry client_id + // to this method when the user opts back in. + context.components.analytics.experiments.resetTelemetryIdentifiers() } else if (key == getPreferenceKey(R.string.pref_key_marketing_telemetry)) { if (context.settings().isMarketingTelemetryEnabled) { context.components.analytics.metrics.start(MetricServiceType.Marketing) diff --git a/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt index ff0515af0..2a9812054 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PrivateBrowsingFragment.kt @@ -5,9 +5,11 @@ package org.mozilla.fenix.settings import android.os.Bundle +import android.view.WindowManager import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.SwitchPreference +import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.metrics.Event @@ -44,7 +46,18 @@ class PrivateBrowsingFragment : PreferenceFragmentCompat() { } requirePreference(R.string.pref_key_allow_screenshots_in_private_mode).apply { - onPreferenceChangeListener = SharedPreferenceUpdater() + onPreferenceChangeListener = object : SharedPreferenceUpdater() { + override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { + if ((activity as? HomeActivity)?.browsingModeManager?.mode?.isPrivate == true && + newValue == false + ) { + activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + } else { + activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + } + return super.onPreferenceChange(preference, newValue) + } + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt index d63bfeee0..54c91d802 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SecretSettingsFragment.kt @@ -6,10 +6,7 @@ package org.mozilla.fenix.settings import android.os.Bundle import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreference -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R -import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar class SecretSettingsFragment : PreferenceFragmentCompat() { @@ -21,11 +18,5 @@ class SecretSettingsFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey) - - requirePreference(R.string.pref_key_synced_tabs_tabs_tray).apply { - isVisible = FeatureFlags.syncedTabsInTabsTray - isChecked = context.settings().syncedTabsInTabsTray - onPreferenceChangeListener = SharedPreferenceUpdater() - } } } 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 2bf9ad3a0..ea8fd5753 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -14,6 +14,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.widget.Toast import androidx.annotation.VisibleForTesting @@ -335,7 +336,7 @@ class SettingsFragment : PreferenceFragmentCompat() { Toast.LENGTH_LONG ).show() - Handler().postDelayed({ + Handler(Looper.getMainLooper()).postDelayed({ exitProcess(0) }, AMO_COLLECTION_OVERRIDE_EXIT_DELAY) } @@ -406,7 +407,7 @@ class SettingsFragment : PreferenceFragmentCompat() { getString(R.string.toast_override_fxa_sync_server_done), Toast.LENGTH_LONG ).show() - Handler().postDelayed({ + Handler(Looper.getMainLooper()).postDelayed({ exitProcess(0) }, FXA_SYNC_OVERRIDE_EXIT_DELAY) } @@ -434,8 +435,14 @@ class SettingsFragment : PreferenceFragmentCompat() { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { Preference.OnPreferenceClickListener { requireContext().getSystemService(RoleManager::class.java).also { - if (!it.isRoleHeld(RoleManager.ROLE_BROWSER)) { - startActivityForResult(it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), 0) + if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld( + RoleManager.ROLE_BROWSER + ) + ) { + startActivityForResult( + it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), + REQUEST_CODE_BROWSER_ROLE + ) } else { navigateUserToDefaultAppsSettings() } @@ -452,12 +459,12 @@ class SettingsFragment : PreferenceFragmentCompat() { else -> { Preference.OnPreferenceClickListener { (activity as HomeActivity).openToBrowserAndLoad( - searchTermOrURL = SupportUtils.getSumoURLForTopic( - requireContext(), - SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER - ), - newTab = true, - from = BrowserDirection.FromSettings + searchTermOrURL = SupportUtils.getSumoURLForTopic( + requireContext(), + SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER + ), + newTab = true, + from = BrowserDirection.FromSettings ) true } @@ -468,12 +475,9 @@ class SettingsFragment : PreferenceFragmentCompat() { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - /* - If role manager doesn't show in-app browser changing dialog for a reason, navigate user to - Default Apps Settings. - */ - if (resultCode == Activity.RESULT_CANCELED && requestCode == 0) { - navigateUserToDefaultAppsSettings() + // If the user made us the default browser, update the switch + if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_BROWSER_ROLE) { + updateMakeDefaultBrowserPreference() } } @@ -547,6 +551,7 @@ class SettingsFragment : PreferenceFragmentCompat() { } 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/logins/fragment/LoginDetailFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/LoginDetailFragment.kt index 63fe12c87..10b031877 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 @@ -13,6 +13,7 @@ import android.view.MenuInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.view.WindowManager import androidx.annotation.StringRes import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment @@ -111,6 +112,14 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) { setHasOptionsMenu(true) } + override fun onResume() { + super.onResume() + activity?.window?.setFlags( + WindowManager.LayoutParams.FLAG_SECURE, + WindowManager.LayoutParams.FLAG_SECURE + ) + } + /** * As described in #10727, the User should re-auth if the fragment is paused and the user is not * navigating to SavedLoginsFragment or EditLoginFragment diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt index 130e856a6..9339dcdbe 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsFragment.kt @@ -150,6 +150,8 @@ class SavedLoginsFragment : Fragment() { toolbarChildContainer.visibility = View.GONE (activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true) sortingStrategyMenu.menuController.dismiss() + sortLoginsMenuRoot.setOnClickListener(null) + setHasOptionsMenu(false) redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id) super.onPause() diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt index 223139995..f6c89acbb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/view/LoginsListViewHolder.kt @@ -5,6 +5,7 @@ package org.mozilla.fenix.settings.logins.view import android.view.View +import androidx.core.view.isVisible import kotlinx.android.synthetic.main.logins_item.* import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.loadIntoView @@ -28,6 +29,7 @@ class LoginsListViewHolder( timeLastUsed = item.timeLastUsed ) webAddressView.text = item.origin + usernameView.isVisible = item.username.isNotEmpty() usernameView.text = item.username updateFavIcon(item.origin) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt index 056db2e20..2caf3fc60 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt @@ -234,17 +234,17 @@ sealed class AutoplayValue( val rules = settings.getSitePermissionsCustomSettingsRules() return listOf( AllowAll( - context.getString(R.string.preference_option_autoplay_allowed2), + context.getString(R.string.quick_setting_option_autoplay_allowed), rules, sitePermission ), BlockAll( - context.getString(R.string.preference_option_autoplay_blocked3), + context.getString(R.string.quick_setting_option_autoplay_blocked), rules, sitePermission ), BlockAudible( - context.getString(R.string.preference_option_autoplay_block_audio2), + context.getString(R.string.quick_setting_option_autoplay_block_audio), rules, sitePermission ) diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt index 8cc26d7fb..dc1735d33 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt @@ -12,6 +12,7 @@ import android.widget.ArrayAdapter import android.widget.TextView import androidx.annotation.VisibleForTesting import androidx.appcompat.widget.AppCompatSpinner +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.quicksettings_permissions.view.* @@ -151,11 +152,34 @@ class WebsitePermissionsView( } val selectedIndex = permissionState.options.indexOf(permissionState.autoplayValue) - val adapter = ArrayAdapter( + + val adapter = object : ArrayAdapter( context, R.layout.quicksettings_permission_spinner_item, permissionState.options - ) + ) { + override fun getDropDownView( + position: Int, + convertView: View?, + parent: ViewGroup + ): View { + val view = super.getDropDownView( + position, + convertView, + parent + ) + if (position == viewHolder.status.selectedItemPosition) { + view.setBackgroundColor( + ContextCompat.getColor( + context, + R.color.spinner_selected_item + ) + ) + } + return view + } + } + adapter.setDropDownViewResource(R.layout.quicksetting_permission_spinner_dropdown) viewHolder.status.adapter = adapter diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/Extensions.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/Extensions.kt index 4c7daaed0..4f459f84e 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/Extensions.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/Extensions.kt @@ -13,12 +13,20 @@ import org.mozilla.fenix.components.Components import org.mozilla.fenix.settings.PhoneFeature /** - * Try to reload a session if a session with the given [origin] it is found. - * @param origin The origin the session to be reloaded. + * Reloads the last used tab matching the provided origin. For performance + * reasons we don't want to reload all matching tabs. Reloading the last used + * tab is a good compromise as it's likely the reason for a change in site + * permissions. + * + * @param origin The origin of the tab to reload. */ internal fun Components.tryReloadTabBy(origin: String) { - val session = core.sessionManager.all.find { it.url.toUri().host == origin } - useCases.sessionUseCases.reload(session) + core.store.state.tabs + .sortedByDescending { it.lastAccess } + .find { it.content.url.toUri().host == origin } + ?.let { + useCases.sessionUseCases.reload(it.id) + } } internal fun initBlockedByAndroidView(phoneFeature: PhoneFeature, blockedByAndroidView: View) { diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt index 8acfbab99..2c5018629 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsFragment.kt @@ -17,7 +17,6 @@ import mozilla.components.browser.storage.sync.Tab import mozilla.components.feature.syncedtabs.SyncedTabsFeature import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.ext.components @@ -28,11 +27,6 @@ import org.mozilla.fenix.theme.ThemeManager class SyncedTabsFragment : LibraryPageFragment() { private val syncedTabsFeature = ViewBoundFeatureWrapper() - init { - // Sanity-check: Remove this class when the feature flag is always enabled. - FeatureFlags.syncedTabsInTabsTray - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, diff --git a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt index 56255910c..b6c65b90e 100644 --- a/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt +++ b/app/src/main/java/org/mozilla/fenix/sync/SyncedTabsLayout.kt @@ -19,7 +19,6 @@ import kotlinx.coroutines.launch import mozilla.components.browser.storage.sync.SyncedDeviceTabs import mozilla.components.browser.storage.sync.Tab import mozilla.components.feature.syncedtabs.view.SyncedTabsView -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController @@ -47,9 +46,6 @@ class SyncedTabsLayout @JvmOverloads constructor( synced_tabs_list.adapter = adapter synced_tabs_pull_to_refresh.setOnRefreshListener { listener?.onRefresh() } - - // Sanity-check: Remove this class when the feature flag is always enabled. - FeatureFlags.syncedTabsInTabsTray } override fun onError(error: SyncedTabsView.ErrorType) { diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt deleted file mode 100644 index b982ca21a..000000000 --- a/app/src/main/java/org/mozilla/fenix/tabtray/SyncedTabsController.kt +++ /dev/null @@ -1,87 +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.tabtray - -import android.view.View -import androidx.fragment.app.FragmentManager.findFragment -import androidx.lifecycle.LifecycleOwner -import androidx.navigation.NavController -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ConcatAdapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.launch -import mozilla.components.browser.storage.sync.SyncedDeviceTabs -import mozilla.components.feature.syncedtabs.view.SyncedTabsView -import mozilla.components.lib.state.ext.flowScoped -import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged -import org.mozilla.fenix.components.metrics.MetricController -import org.mozilla.fenix.sync.ListenerDelegate -import org.mozilla.fenix.sync.SyncedTabsAdapter -import org.mozilla.fenix.sync.ext.toAdapterList -import org.mozilla.fenix.sync.ext.toAdapterItem -import org.mozilla.fenix.sync.ext.toStringRes -import kotlin.coroutines.CoroutineContext - -@OptIn(ExperimentalCoroutinesApi::class) -class SyncedTabsController( - lifecycleOwner: LifecycleOwner, - private val view: View, - store: TabTrayDialogFragmentStore, - private val concatAdapter: ConcatAdapter, - coroutineContext: CoroutineContext = Dispatchers.Main, - metrics: MetricController -) : SyncedTabsView { - override var listener: SyncedTabsView.Listener? = null - - val adapter = SyncedTabsAdapter(ListenerDelegate(metrics) { listener }) - - private val scope: CoroutineScope = CoroutineScope(coroutineContext) - - init { - store.flowScoped(lifecycleOwner) { flow -> - flow.map { it.mode } - .ifChanged() - .drop(1) - .collect { mode -> - when (mode) { - is TabTrayDialogFragmentState.Mode.Normal -> { - concatAdapter.addAdapter(adapter) - } - is TabTrayDialogFragmentState.Mode.MultiSelect -> { - concatAdapter.removeAdapter(adapter) - } - } - } - } - } - - override fun displaySyncedTabs(syncedTabs: List) { - scope.launch { - val tabsList = listOf(SyncedTabsAdapter.AdapterItem.Title) + syncedTabs.toAdapterList() - // Reverse layout for TabTrayView which does things backwards. - adapter.submitList(tabsList.reversed()) - } - } - - override fun onError(error: SyncedTabsView.ErrorType) { - scope.launch { - val navController: NavController? = try { - findFragment(view).findNavController() - } catch (exception: IllegalStateException) { - null - } - - val descriptionResId = error.toStringRes() - val errorItem = error.toAdapterItem(descriptionResId, navController) - - adapter.submitList(listOf(errorItem)) - } - } -} diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt index f38e01d96..4ce4c635c 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayController.kt @@ -20,7 +20,6 @@ import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.storage.BookmarksStorage import mozilla.components.concept.tabstray.Tab import mozilla.components.feature.tabs.TabsUseCases -import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager @@ -28,7 +27,6 @@ import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.home.HomeFragment -import mozilla.components.browser.storage.sync.Tab as SyncTab /** * [TabTrayDialogFragment] controller. @@ -42,7 +40,6 @@ interface TabTrayController { fun handleTabSettingsClicked() fun handleShareTabsOfTypeClicked(private: Boolean) fun handleShareSelectedTabsClicked(selectedTabs: Set) - fun handleSyncedTabClicked(syncTab: SyncTab) fun handleSaveToCollectionClicked(selectedTabs: Set) fun handleBookmarkSelectedTabs(selectedTabs: Set) fun handleDeleteSelectedTabs(selectedTabs: Set) @@ -197,14 +194,6 @@ class DefaultTabTrayController( } } - override fun handleSyncedTabClicked(syncTab: SyncTab) { - activity.openToBrowserAndLoad( - searchTermOrURL = syncTab.active().url, - newTab = true, - from = BrowserDirection.FromTabTray - ) - } - @OptIn(ExperimentalCoroutinesApi::class) override fun handleCloseAllTabsClicked(private: Boolean) { val sessionsToClose = if (private) { diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt index fbf222d8a..ef6f3fe70 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayDialogFragment.kt @@ -32,7 +32,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.plus import mozilla.appservices.places.BookmarkRoot -import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.normalTabs @@ -45,6 +44,9 @@ import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.view.showKeyboard +import mozilla.components.support.utils.ext.bottom +import mozilla.components.support.utils.ext.left +import mozilla.components.support.utils.ext.right import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R @@ -105,12 +107,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler requireComponents.useCases.tabsUseCases.selectTab(tabId) navigateToBrowser() } - - override fun invoke(session: Session) { - requireContext().components.analytics.metrics.track(Event.OpenedExistingTab) - requireComponents.useCases.tabsUseCases.selectTab(session) - navigateToBrowser() - } } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -127,12 +123,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler showUndoSnackbarForTab(sessionId) removeIfNotLastTab(sessionId) } - - override fun invoke(session: Session) { - requireContext().components.analytics.metrics.track(Event.ClosedExistingTab) - showUndoSnackbarForTab(session.id) - removeIfNotLastTab(session.id) - } } private fun removeIfNotLastTab(sessionId: String) { @@ -225,7 +215,6 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler showBookmarksSnackbar = ::showBookmarksSnackbar ) ), - store = tabTrayDialogStore, isPrivate = isPrivate, isInLandscape = ::isInLandscape, lifecycleOwner = viewLifecycleOwner @@ -257,13 +246,13 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler view.tabLayout.setOnApplyWindowInsetsListener { v, insets -> v.updatePadding( - left = insets.systemWindowInsetLeft, - right = insets.systemWindowInsetRight, - bottom = insets.systemWindowInsetBottom + left = insets.left(), + right = insets.right(), + bottom = insets.bottom() ) tabTrayView.view.tab_wrapper.updatePadding( - bottom = insets.systemWindowInsetBottom + bottom = insets.bottom() ) insets @@ -281,7 +270,7 @@ class TabTrayDialogFragment : AppCompatDialogFragment(), UserInteractionHandler private fun setSecureFlagsIfNeeded(private: Boolean) { if (private && context?.settings()?.allowScreenshotsInPrivateMode == false) { dialog?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else if (!(activity as HomeActivity).browsingModeManager.mode.isPrivate) { + } else { dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) } } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt index 8dc718225..7f7eae7a2 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractor.kt @@ -5,7 +5,6 @@ package org.mozilla.fenix.tabtray import mozilla.components.concept.tabstray.Tab -import mozilla.components.browser.storage.sync.Tab as SyncTab @Suppress("TooManyFunctions") interface TabTrayInteractor { @@ -54,11 +53,6 @@ interface TabTrayInteractor { */ fun onCloseAllTabsClicked(private: Boolean) - /** - * Called when the user clicks on a synced tab entry. - */ - fun onSyncedTabClicked(syncTab: SyncTab) - /** * Called when the physical back button is clicked. */ @@ -146,10 +140,6 @@ class TabTrayFragmentInteractor(private val controller: TabTrayController) : Tab controller.handleCloseAllTabsClicked(private) } - override fun onSyncedTabClicked(syncTab: SyncTab) { - controller.handleSyncedTabClicked(syncTab) - } - override fun onBackPressed(): Boolean { return controller.handleBackPressed() } diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt index bf97ba942..db3768a32 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayView.kt @@ -41,15 +41,13 @@ import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.tabstray.TabViewHolder -import mozilla.components.feature.syncedtabs.SyncedTabsFeature -import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.ktx.android.util.dpToPx import mozilla.components.ui.tabcounter.TabCounter.Companion.INFINITE_CHAR_PADDING_BOTTOM import org.mozilla.fenix.R -import org.mozilla.fenix.browser.InfoBanner import org.mozilla.fenix.components.metrics.Event import mozilla.components.ui.tabcounter.TabCounter.Companion.MAX_VISIBLE_TABS import mozilla.components.ui.tabcounter.TabCounter.Companion.SO_MANY_TABS_OPEN +import org.mozilla.fenix.browser.infobanner.InfoBanner import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.updateAccessibilityCollectionInfo @@ -59,7 +57,6 @@ import org.mozilla.fenix.utils.Settings import java.text.NumberFormat import kotlin.math.max import kotlin.math.roundToInt -import mozilla.components.browser.storage.sync.Tab as SyncTab /** * View that contains and configures the BrowserAwesomeBar @@ -69,7 +66,6 @@ class TabTrayView( private val container: ViewGroup, private val tabsAdapter: FenixTabsAdapter, private val interactor: TabTrayInteractor, - store: TabTrayDialogFragmentStore, isPrivate: Boolean, private val isInLandscape: () -> Boolean, lifecycleOwner: LifecycleOwner, @@ -97,11 +93,6 @@ class TabTrayView( private var tabsTouchHelper: TabsTouchHelper private val collectionsButtonAdapter = SaveToCollectionsButtonAdapter(interactor, isPrivate) - private val metrics = container.context.components.analytics.metrics - - private val syncedTabsController = - SyncedTabsController(lifecycleOwner, view, store, concatAdapter, metrics = metrics) - private val syncedTabsFeature = ViewBoundFeatureWrapper() private var hasLoaded = false @@ -167,21 +158,6 @@ class TabTrayView( setTopOffset(isInLandscape()) - if (view.context.settings().syncedTabsInTabsTray) { - syncedTabsFeature.set( - feature = SyncedTabsFeature( - context = container.context, - storage = components.backgroundServices.syncedTabsStorage, - accountManager = components.backgroundServices.accountManager, - view = syncedTabsController, - lifecycleOwner = lifecycleOwner, - onTabClicked = ::handleTabClicked - ), - owner = lifecycleOwner, - view = view - ) - } - updateTabsTrayLayout() view.tabsTray.apply { @@ -197,7 +173,6 @@ class TabTrayView( tabsAdapter.tabTrayInteractor = interactor tabsAdapter.onTabsUpdated = { concatAdapter.addAdapter(collectionsButtonAdapter) - concatAdapter.addAdapter(syncedTabsController.adapter) if (hasAccessibilityEnabled) { tabsAdapter.notifyItemRangeChanged(0, tabs.size) @@ -344,10 +319,6 @@ class TabTrayView( } } - private fun handleTabClicked(tab: SyncTab) { - interactor.onSyncedTabClicked(tab) - } - private fun adjustNewTabButtonsForNormalMode() { view.tab_tray_new_tab.apply { isVisible = hasAccessibilityEnabled diff --git a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt index 1965f32b1..5d0832433 100644 --- a/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/tabtray/TabTrayViewHolder.kt @@ -36,12 +36,6 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showAndEnable import org.mozilla.fenix.ext.toShortUrl import kotlin.math.max -import mozilla.components.browser.state.state.MediaState -import mozilla.components.feature.media.ext.pauseIfPlaying -import mozilla.components.feature.media.ext.playIfPaused -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi -import org.mozilla.fenix.ext.getMediaStateForSession -import org.mozilla.fenix.utils.Do /** * A RecyclerView ViewHolder implementation for "tab" items. @@ -99,94 +93,46 @@ class TabTrayViewHolder( // Media state playPauseButtonView.increaseTapArea(PLAY_PAUSE_BUTTON_EXTRA_DPS) - if (newMediaSessionApi) { - 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() - } + 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) + ) } - 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." - ) - } + 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 { - with(playPauseButtonView) { - invalidate() - Do exhaustive when (store.state.getMediaStateForSession(tab.id)) { - MediaState.State.PAUSED -> { - showAndEnable() - contentDescription = - context.getString(R.string.mozac_feature_media_notification_action_play) - setImageDrawable( - AppCompatResources.getDrawable(context, R.drawable.media_state_play) - ) - } - - MediaState.State.PLAYING -> { - showAndEnable() - contentDescription = - context.getString(R.string.mozac_feature_media_notification_action_pause) - setImageDrawable( - AppCompatResources.getDrawable(context, R.drawable.media_state_pause) - ) - } - MediaState.State.NONE -> { - removeTouchDelegate() - removeAndDisable() - } + else -> { + removeTouchDelegate() + removeAndDisable() } } - playPauseButtonView.setOnClickListener { - Do exhaustive when (store.state.getMediaStateForSession(tab.id)) { - MediaState.State.PLAYING -> { + setOnClickListener { + when (sessionState?.mediaSessionState?.playbackState) { + MediaSession.PlaybackState.PLAYING -> { metrics.track(Event.TabMediaPause) - store.state.media.pauseIfPlaying() + sessionState.mediaSessionState?.controller?.pause() } - MediaState.State.PAUSED -> { + MediaSession.PlaybackState.PAUSED -> { metrics.track(Event.TabMediaPlay) - store.state.media.playIfPaused() + sessionState.mediaSessionState?.controller?.play() } - - MediaState.State.NONE -> throw AssertionError( + else -> throw AssertionError( "Play/Pause button clicked without play/pause state." ) } diff --git a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt index 6e098fa12..167c78d18 100644 --- a/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt +++ b/app/src/main/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlay.kt @@ -62,9 +62,9 @@ class TrackingProtectionOverlay( }.ifChanged { tab -> tab.content.loading } - .collect { tab -> - onLoadingStateChanged(tab) - } + .collect { tab -> + onLoadingStateChanged(tab) + } } } @@ -77,7 +77,10 @@ class TrackingProtectionOverlay( @VisibleForTesting internal fun onLoadingStateChanged(tab: SessionState) { - if (!tab.content.loading && shouldShowTrackingProtectionOnboarding(tab)) { + if (shouldShowTrackingProtectionOnboarding(tab) && + tab.content.progress == FULL_PROGRESS && + settings.shouldUseTrackingProtection + ) { showTrackingProtectionOnboarding() } } @@ -85,7 +88,7 @@ class TrackingProtectionOverlay( private fun shouldShowTrackingProtectionOnboarding(tab: SessionState) = tab.trackingProtection.enabled && tab.trackingProtection.blockedTrackers.isNotEmpty() && - settings.shouldShowTrackingProtectionCfr + settings.shouldShowTrackingProtectionCfr @Suppress("MagicNumber", "InflateParams") private fun showTrackingProtectionOnboarding() { @@ -184,6 +187,7 @@ class TrackingProtectionOverlay( } private companion object { + private const val FULL_PROGRESS = 100 private const val BUTTON_INCREASE_DPS = 12 } } diff --git a/app/src/main/java/org/mozilla/fenix/utils/ManufacturerCodes.kt b/app/src/main/java/org/mozilla/fenix/utils/ManufacturerCodes.kt new file mode 100644 index 000000000..7915bcf4f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/utils/ManufacturerCodes.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.utils + +import android.os.Build + +/** + * A listing of codes returned by [android.os.Build.MANUFACTURER] for different manufacturers. + * While we try to get the casing accurate, it may be good to use .equals(str, ignoreCase = true) + * to do the comparison. + */ +object ManufacturerCodes { + const val HUAWEI: String = "HUAWEI" + const val LG = "LGE" + const val ONE_PLUS: String = "OnePlus" + const val SAMSUNG = "samsung" + + val isLG get() = Build.MANUFACTURER.equals(LG, ignoreCase = true) + val isSamsung get() = Build.MANUFACTURER.equals(SAMSUNG, ignoreCase = true) +} 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 2dca9a789..156dc89a3 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/Settings.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/Settings.kt @@ -26,12 +26,10 @@ import mozilla.components.support.ktx.android.content.longPreference import mozilla.components.support.ktx.android.content.stringPreference import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.Config -import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode 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.ext.components import org.mozilla.fenix.ext.getPreferenceKey @@ -123,12 +121,6 @@ class Settings(private val appContext: Context) : PreferencesHolder { val canShowCfr: Boolean get() = (System.currentTimeMillis() - lastCfrShownTimeInMillis) > THREE_DAYS_MS - var syncedTabsInTabsTray by featureFlagPreference( - appContext.getPreferenceKey(R.string.pref_key_synced_tabs_tabs_tray), - default = false, - featureFlag = FeatureFlags.syncedTabsInTabsTray - ) - var forceEnableZoom by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_accessibility_force_enable_zoom), default = false @@ -252,6 +244,11 @@ class Settings(private val appContext: Context) : PreferencesHolder { val shouldShowSecurityPinWarning: Boolean get() = loginsSecureWarningCount.underMaxCount() + var shouldShowPrivacyPopWindow by booleanPreference( + appContext.getPreferenceKey(R.string.pref_key_privacy_pop_window), + default = true + ) + var shouldUseLightTheme by booleanPreference( appContext.getPreferenceKey(R.string.pref_key_light_theme), default = false @@ -471,6 +468,13 @@ class Settings(private val appContext: Context) : PreferencesHolder { true ) + /** + * Prefer to use a fixed top toolbar when: + * - a talkback service is enabled or + * - switch access is enabled. + * + * This is automatically inferred based on the current system status. Not a setting in our app. + */ val shouldUseFixedTopToolbar: Boolean get() { return touchExplorationIsEnabled || switchServiceIsEnabled diff --git a/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt index 21c56576b..f9b349fce 100644 --- a/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt +++ b/app/src/main/java/org/mozilla/fenix/utils/ToolbarPopupWindow.kt @@ -15,8 +15,8 @@ import androidx.annotation.VisibleForTesting import androidx.core.view.isVisible import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.browser_toolbar_popup_window.view.* -import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.selectedTab +import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.store.BrowserStore import org.mozilla.fenix.R import org.mozilla.fenix.components.FenixSnackbar @@ -27,7 +27,7 @@ import java.lang.ref.WeakReference object ToolbarPopupWindow { fun show( view: WeakReference, - customTabSession: Session? = null, + customTabSession: CustomTabSessionState? = null, handlePasteAndGo: (String) -> Unit, handlePaste: (String) -> Unit, copyVisible: Boolean = true @@ -101,10 +101,10 @@ object ToolbarPopupWindow { @VisibleForTesting internal fun getUrlForClipboard( store: BrowserStore, - customTabSession: Session? = null + customTabSession: CustomTabSessionState? = null ): String? { return if (customTabSession != null) { - customTabSession.url + customTabSession.content.url } else { val selectedTab = store.state.selectedTab selectedTab?.readerState?.activeUrl ?: selectedTab?.content?.url diff --git a/app/src/main/res/drawable/etp_spinner_item_background.xml b/app/src/main/res/drawable/etp_spinner_item_background.xml index 642b276e3..05f0f2eea 100644 --- a/app/src/main/res/drawable/etp_spinner_item_background.xml +++ b/app/src/main/res/drawable/etp_spinner_item_background.xml @@ -5,7 +5,7 @@ - + diff --git a/app/src/main/res/layout/activity_privacy_content_display.xml b/app/src/main/res/layout/activity_privacy_content_display.xml new file mode 100644 index 000000000..c6ff55e47 --- /dev/null +++ b/app/src/main/res/layout/activity_privacy_content_display.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/component_browser_top_toolbar.xml b/app/src/main/res/layout/component_browser_top_toolbar.xml index 479bc9dd9..77f460296 100644 --- a/app/src/main/res/layout/component_browser_top_toolbar.xml +++ b/app/src/main/res/layout/component_browser_top_toolbar.xml @@ -2,12 +2,9 @@ - + - - diff --git a/app/src/main/res/layout/fragment_browser.xml b/app/src/main/res/layout/fragment_browser.xml index 63150e569..32d2bd4c4 100644 --- a/app/src/main/res/layout/fragment_browser.xml +++ b/app/src/main/res/layout/fragment_browser.xml @@ -25,8 +25,7 @@ + android:layout_height="match_parent"> + android:background="?attr/scrimBackground"> - - - - + app:layout_constraintTop_toBottomOf="@id/toolbar" + app:layout_constraintHeight_default="wrap"/> + + + + - - - + android:layout_height="match_parent" + android:scrollbars="vertical" + android:fadeScrollbars="false" + app:layout_constraintBottom_toBottomOf="@id/search_divider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/toolbar"> - + android:background="?attr/foundation" + android:paddingStart="20dp" + android:paddingTop="20dp" + android:paddingEnd="20dp" + android:paddingBottom="10dp"> - + - + - + + + + + - + - + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 07666ee3c..d3a9c5efb 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -102,7 +102,11 @@ app:destination="@id/trackingProtectionExceptionsFragment" /> + app:destination="@id/accountSettingsFragment" + app:enterAnim="@anim/slide_in_right" + app:exitAnim="@anim/slide_out_left" + app:popEnterAnim="@anim/slide_in_left" + app:popExitAnim="@anim/slide_out_right" /> diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index d13fff47d..54b3faa96 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -130,6 +130,8 @@ Amestar a marcadores Complementos + + Estensiones Equí nun hai complementos @@ -289,6 +291,8 @@ Modificóse\'l sirvidor de cuentes/sincronización de Firefox. Colando de l\'aplicación p\'aplicar los cambeos… Cuenta + + Aniciar sesión Barra de ferramientes @@ -351,6 +355,8 @@ Encaboxar + + Nome de la coleición Modificóse la coleición de complementos. L\'aplicación va zarrase p\'aplicar los cambeos… @@ -454,6 +460,8 @@ Activación de Sync + + Aniciar sesión Aniciar sesión pa reconectar @@ -512,13 +520,19 @@ Historial Llingüeta nueva + + Atopar na llibrería Llingüetes sincronizaes Llista de llectura + + Buscar Axustes + + Menú d\'un elementu del historial Zarrar @@ -595,6 +609,8 @@ Zarrar toles llingüetes Llingüeta nueva + + Dir pal aniciu Amestar a Marcadores @@ -677,6 +693,9 @@ Elementos esbillaos: %1$d + + Desaniciar %1$d elementos Güei @@ -707,6 +726,8 @@ Descargues esbillaes: %1$d + + Abrir Quitar @@ -728,6 +749,10 @@ Menú de los marcadores + + Edición d\'un marcador + + Esbilla d\'una carpeta ¿De xuru que quies desaniciar esta carpeta? @@ -806,6 +831,8 @@ Aconséyase Reafitamientu de permisos + + Reafitar el permisu Reafitar los permisos de tolos sitios @@ -838,16 +865,22 @@ Non - + Permitir l\'audiu y videu + + Permitir l\'audiu y videu Bloquiar namás l\'audiu y videu colos datos móviles L\'audiu y videu va reproducise namás per Wi-Fi - + Bloquiar namás l\'audiu - + + Bloquiar namás l\'audiu + Bloquiar l\'audiu y videu + + Bloquiar l\'audiu y videu @@ -920,6 +953,8 @@ Unviar a tolos preseos Reconexón con Sync + + Ensin conexón Coneutar otru preséu @@ -1156,7 +1191,7 @@ Estricta - Bloquia más rastrexadores, anuncios y ventanos emerxentes. Los páxines van cargar más rápido mas dalgunes funcionalidaes quiciabes nun funcionen. + Bloquia más rastrexadores, anuncios y ventanos emerxentes. Les páxines van cargar más rápido mas dalgunes funcionalidaes quiciabes nun funcionen. @@ -1603,6 +1638,8 @@ en llinia y con nós. El campu de testu editable pal nome d\'usuariu del aniciu de sesión. El campu de testu editable pa la contraseña del aniciu de sesión. + + Escartar los cambeos Edición diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index d55cb5f6a..5cd09eab8 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -128,6 +128,8 @@ Рэдагаваць закладку Дадаткі + + Пашырэнні Няма дадаткаў @@ -214,6 +216,11 @@ Даведацца больш + + Пошук у %s + + Пошук наўпрост з адраснага радка + Адкрыць новую картку Firefox @@ -531,6 +538,10 @@ Іншыя закладкі Гісторыя + + Новая картка + + Знайсці на старонцы Сінхранізаваныя карткі @@ -888,16 +899,22 @@ Уключана Выключана - + Дазволіць гук і відэа + + Дазволіць гук і відэа Блакаваць аўдыё і відэа толькі на мабільных дадзеных Аўдыё і відэа будуць прайгравацца праз Wi-Fi - + Блакаваць толькі гук - + + Блакаваць толькі гук + Блакаваць гук і відэа + + Блакаваць гук і відэа Уключана diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1a141ea80..5dbe8b20e 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -128,6 +128,8 @@ Embann ar sined Askouezhioù + + Askouezhioù Nʼeus tamm askouezh ebet amañ @@ -212,6 +214,11 @@ Gouzout hirocʼh + + Klask %s + + Klask war-eeun eus ar varrenn chomlec’h + Digeriñ un ivinell Firefox nevez @@ -523,6 +530,10 @@ Sinedoù all Roll istor + + Ivinell nevez + + Kavout er bajenn Ivinelloù goubredet @@ -873,17 +884,23 @@ Gweredekaet Diweredekaet - + Aotren an aodio ha video + + Aotren an aodio ha video Stankañ an aodio hag ar video pa vez war ar roadennoù hezoug nemetken An aodio hag ar video a vo lennet war ar Wi-Fi - + Stankañ an aodio nemetken - + + Stankañ an aodio nemetken + Stankañ an aodio hag ar video + + Stankañ an aodio hag ar video Gweredekaet diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index b4b7cb5ff..ff2514abc 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -218,6 +218,11 @@ Més informació + + Cerca amb %s + + Cerca directament des de la barra d’adreces + Obre una pestanya nova del Firefox @@ -889,16 +894,22 @@ Activada Desactivada - + Permet àudio i vídeo + + Permet àudio i vídeo Bloca àudio i vídeo només amb dades mòbils L’àudio i el vídeo es reproduiran amb Wi-Fi - + Bloca només àudio - + + Bloca només àudio + Bloca àudio i vídeo + + Bloca àudio i vídeo Activat diff --git a/app/src/main/res/values-cak/strings.xml b/app/src/main/res/values-cak/strings.xml index 7d7db3b5c..1cea4e24a 100644 --- a/app/src/main/res/values-cak/strings.xml +++ b/app/src/main/res/values-cak/strings.xml @@ -135,6 +135,8 @@ Taq tz\'aqat + + Taq k\'amal Majun tz\'aqat wakami @@ -185,6 +187,8 @@ Rutzub\'al + + Ke\'ichinäx rutz\'etoj sik\'inem Man nok ta. Man netamäx ta ruwa ruch\'akulal URL. @@ -222,6 +226,12 @@ Tetamäx ch\'aqa\' chik + + Tikanöx %s + + + Tikanöx pa kikajtz\'ik ochochib\'äl + Tijaq jun k\'ak\'a\' ruwi\' Firefox @@ -547,6 +557,10 @@ Ch\'aqa\' chik taq Yaketal Natab\'äl + + K\'ak\'a\' ruwi\' + + Tikanöx pa ruxaq Ximon taq ruwi\' @@ -904,16 +918,22 @@ Titzij Tichup - + Tiya\' q\'ij k\'oxom chuqa\' silowäch + + Tiya\' q\'ij k\'oxom chuqa\' silowäch Keq\'at k\'oxom chuqa\' silowäch xa xe rik\'in kitzij selular Ri k\'oxom chuqa\' silowäch xketzij rik\'in Wi-Fi - + Xa xe tiq\'at k\'oxom - + + Xa xe tiq\'at k\'oxom + Tiq\'at k\'oxom chuqa\' silowäch + + Tiq\'at k\'oxom chuqa\' silowäch Titzij @@ -1118,6 +1138,8 @@ Taq ruya\'oj q\'ij ruxaq + + Taq qasanïk Keyuj taq rutzij okem pa k\'amaya\'l @@ -1243,8 +1265,7 @@ - Xqawachib\'ej %s richin nachajij ri nakomonij - pa k\'amab\'ey chuqa\' ri nakomonij qik\'in. + Xqawachib\'ej %s richin nachajij ri nakomonij pa k\'amab\'ey chuqa\' ri nakomonij qik\'in. Tasik\'ij ri rutzijol qichinanem diff --git a/app/src/main/res/values-co/strings.xml b/app/src/main/res/values-co/strings.xml index c42c30624..a8b76440e 100644 --- a/app/src/main/res/values-co/strings.xml +++ b/app/src/main/res/values-co/strings.xml @@ -185,6 +185,8 @@ Aspettu + + Persunalizà u modu di lettura Impussibule di cunnettassi. Schema d’URL scunnisciutu. @@ -221,6 +223,11 @@ Sapene di più + + Ricercà in %s + + Ricercà direttamente da a barra d’indirizzu + Apre una nova unghjetta in Firefox @@ -897,16 +904,22 @@ Attivata Disattivata - + Permette l’audio è a video + + Permette l’audio è a video Bluccà l’audio è a video da i dati mubili solu Audio è video seranu letti cù una cunnessione Wi-Fi - + Bluccà solu l’audio - + + Bluccà solu l’audio + Bluccà l’audio è a video + + Bluccà l’audio è a video Attivata diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5d3a4477e..6b80e461f 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -222,6 +222,11 @@ Zjistit více + + Hledat na %s + + Vyhledat na webu přímo z adresního řádku + Otevřít nový panel Firefoxu @@ -905,16 +910,22 @@ Vypnuta - + Povolit zvuk i video + + Povolit zvuk i video Blokovat automatické přehrávání zvuků i videí při připojení přes mobilní data Na Wi-Fi připojení se zvuky i videa automaticky přehrávat budou - + Blokovat automatické přehrávání zvuků - + + Blokovat automatické přehrávání zvuků + Blokovat automatické přehrávání zvuků i videí + + Blokovat automatické přehrávání zvuků i videí Zapnuto @@ -1114,6 +1125,8 @@ Uvolní využitý prostor na zařízení Oprávnění serverů + + Stažené soubory Smazat soukromá data diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 1ee1be2a5..336188a71 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -180,6 +180,8 @@ Gwedd + + Cyfaddasu’r golwg defnyddiwr Methu cysylltu. Cynllun URL anadnabyddadwy. @@ -216,6 +218,11 @@ Dysgu rhagor + + Chwilio %s + + Chwilio’n uniongyrchol o’r bar cyfeiriad + Agorwch tab Firefox newydd @@ -886,16 +893,22 @@ Ymlaen Diffodd - + Caniatáu sain a fideo + + Caniatáu sain a fideo Rhwystro sain a fideo ar ddata cellol yn unig Bydd sain a fideo yn chwarae ar ddiwifr - + Rhwystro sain yn unig - + + Rhwystro sain yn unig + Rhwystro sain a fideo + + Rhwystro sain a fideo Ymlaen diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 838fe1f53..b662bf04c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -130,6 +130,8 @@ Rediger bogmærke Tilføjelser + + Udvidelser Her er ingen tilføjelser @@ -523,6 +525,10 @@ Andre bogmærker Historik + + Nyt faneblad + + Find på siden Synkroniserede faneblade @@ -873,16 +879,22 @@ Til Fra - + Tillad lyd og video + + Tillad lyd og video Bloker kun lyd og video via mobildata Lyd og video afspilles kun via wi-fi - + Bloker kun lyd - + + Bloker kun lyd + Bloker lyd og video + + Bloker lyd og video Til diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 0f8097498..69ab06941 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -185,6 +185,8 @@ Erscheinungsbild + + Leseansicht anpassen Verbindung nicht möglich. Nicht erkennbares URL-Schema. @@ -222,6 +224,11 @@ Weitere Informationen + + Suchen mit %s + + Direkt aus der Adressleiste suchen + In einem neuen Firefox-Tab öffnen @@ -902,16 +909,22 @@ Aus - + Audio und Video erlauben + + Audio und Video erlauben Audio und Video nur bei Mobilfunkverbindung blockieren Audio und Video werden bei WLAN-Verbindung wiedergegeben - + Nur Audio blockieren - + + Nur Audio blockieren + Audio und Video blockieren + + Audio und Video blockieren Ein diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml index 021686f6b..589dc5bdb 100644 --- a/app/src/main/res/values-dsb/strings.xml +++ b/app/src/main/res/values-dsb/strings.xml @@ -180,6 +180,8 @@ Wenkowny naglěd + + Cytański naglěd pśiměriś Zwisk njejo móžny. Njespóznawajobna URL-šema. @@ -216,6 +218,11 @@ Dalšne informacije + + Z %s pytaś + + Direktnje z adresowego póla pytaś + Nowy rejtarik Firefox wócyniś @@ -886,16 +893,22 @@ Zašaltowany Wušaltowany - + Awdio a wideo dowóliś + + Awdio a wideo dowóliś Awdio a wideo jano za mobilny zwisk blokěrowaś Awdio a wideo se pśez WLAN wótgrawatej - + Jano awdio blokěrowaś - + + Jano awdio blokěrowaś + Awdio a wideo blokěrowaś + + Awdio a wideo blokěrowaś Zašaltowany diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 345827d89..1d72264bd 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -186,6 +186,8 @@ Εμφάνιση + + Προσαρμογή προβολής ανάγνωσης Αδυναμία σύνδεσης. Μη αναγνωρίσιμο σχήμα URL. @@ -223,6 +225,11 @@ Μάθετε περισσότερα + + Αναζήτηση %s + + Αναζήτηση απευθείας από τη γραμμή διευθύνσεων + Άνοιγμα νέας καρτέλας Firefox @@ -896,16 +903,22 @@ Ενεργό Ανενεργό - + Αποδοχή ήχου και βίντεο + + Αποδοχή ήχου και βίντεο Φραγή ήχου και βίντεο μόνο σε σύνδεση δεδομένων κινητής Η αναπαραγωγή ήχων/βίντεο θα γίνεται σε Wi-Fi - + Φραγή ήχου μόνο - + + Φραγή ήχου μόνο + Φραγή ήχου και βίντεο + + Φραγή ήχου και βίντεο Ενεργό diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml index 03c68fca0..146c1e22b 100644 --- a/app/src/main/res/values-en-rCA/strings.xml +++ b/app/src/main/res/values-en-rCA/strings.xml @@ -130,6 +130,8 @@ Edit bookmark Add-ons + + Extensions No add-ons here @@ -178,6 +180,8 @@ Appearance + + Customize reader view Unable to connect. Unrecognizable URL scheme. @@ -214,6 +218,11 @@ Learn more + + Search %s + + Search directly from the address bar + Open a new Firefox tab @@ -524,6 +533,10 @@ Other Bookmarks History + + New tab + + Find in page Synced tabs @@ -873,16 +886,22 @@ On Off - + Allow audio and video + + Allow audio and video Block audio and video on cellular data only Audio and video will play on Wi-Fi - + Block audio only - + + Block audio only + Block audio and video + + Block audio and video On diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 49d293b86..9516a3abc 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -179,6 +179,8 @@ Appearance + + Customise reader view Unable to connect. Unrecognisable URL scheme. @@ -215,6 +217,11 @@ Learn more + + Search %s + + Search directly from the address bar + Open a new Firefox tab @@ -881,16 +888,22 @@ On Off - + Allow audio and video + + Allow audio and video Block audio and video on mobile data only Audio and video will play on Wi-Fi - + Block audio only - + + Block audio only + Block audio and video + + Block audio and video On diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index fc99a3785..13986308f 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -890,16 +890,20 @@ Ŝaltita Malŝaltita - + Permesi sonon kaj videon Bloki sonon kaj videon nur dum konekto al poŝaparataj retoj Sono kaj video estos ludataj en sendrata konektoj (Wi-Fi) - + Nur bloki sonon - + + Nur bloki sonon + Bloki sonon kaj videon + + Bloki sonon kaj videon Ŝaltita diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index d2126b692..01f219fe1 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -183,6 +183,8 @@ Apariencia + + Personalizar vista de lectura No se puede conectar. Esquema de URL irreconocible. @@ -222,6 +224,11 @@ Conocer más + + Buscar %s + + Buscar directamente desde la barra de direcciones + Abrir una nueva pestaña de Firefox @@ -905,16 +912,22 @@ Desactivada - + Permitir audio y video + + Permitir audio y video Bloquear audio y video solo en datos celulares El audio y el video se van a reproducir con Wi-Fi - + Bloquear solo audio - + + Bloquear solo audio + Bloquear audio y video + + Bloquear audio y video Activado diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml index de1528334..55e449df7 100644 --- a/app/src/main/res/values-es-rCL/strings.xml +++ b/app/src/main/res/values-es-rCL/strings.xml @@ -129,6 +129,8 @@ Editar marcador Complementos + + Extensiones No hay complementos aquí @@ -213,6 +215,11 @@ Aprender más + + Buscar %s + + Buscar directamente desde la barra de direcciones + Abrir una nueva pestaña de Firefox @@ -526,6 +533,10 @@ Otros marcadores Historial + + Nueva pestaña + + Buscar en la página Pestañas sincronizadas @@ -877,16 +888,22 @@ No - + Permitir audio y video + + Permitir audio y video Bloquear audio y video solo en datos celulares El audio y video se reproducirán en Wi-Fi - + Bloquear solo audio - + + Bloquear solo audio + Bloquear audio y video + + Bloquear audio y video diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml index 0f8730760..41c69bdc0 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -184,6 +184,8 @@ Apariencia + + Personalizar vista de lectura No se puede conectar. Esquema de URL irreconocible. @@ -221,6 +223,11 @@ Saber más + + Buscar %s + + Buscar directamente desde la barra de direcciones + Abrir una nueva pestaña de Firefox @@ -900,16 +907,22 @@ Desactivada - + Permitir audio y vídeo + + Permitir audio y vídeo Bloquear audio y vídeo solo con datos móviles El audio y el vídeo se reproducirán con Wi-Fi - + Bloquear solo audio - + + Bloquear solo audio + Bloquear audio y vídeo + + Bloquear audio y vídeo Activado diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ad65854cf..4529d97c8 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -136,6 +136,8 @@ Editar marcador Complementos + + Extensiones No hay complementos aquí @@ -222,6 +224,9 @@ Saber más + + Buscar %1$s + Abrir una nueva pestaña de Firefox @@ -537,6 +542,10 @@ Otros marcadores Historial + + Nueva pestaña + + Buscar en la página Pestañas sincronizadas @@ -894,16 +903,22 @@ Desactivado - + Permitir audio y vídeo + + Permitir audio y video Bloquear audio y vídeo solo con datos móviles El audio y el vídeo se reproducirán con Wi-Fi - + Bloquear solo audio - + + Bloquear solo audio + Bloquear audio y vídeo + + Bloquear audio y video Activado diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 2e8280356..7dd70b032 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -132,6 +132,8 @@ Editatu laster-marka Gehigarriak + + Hedapenak Gehigarririk ez hemen @@ -183,6 +185,8 @@ Itxura + + Pertsonalizatu irakurtzeko ikuspegia Ezin da konektatu. URL eskema ezagutezina. @@ -220,6 +224,11 @@ Argibide gehiago + + Bilatu %s erabiliz + + Bilatu helbide-barratik zuzenean + Ireki Firefoxen fitxa berri bat @@ -538,6 +547,10 @@ Beste laster-markak Historia + + Fitxa berria + + Bilatu orrian Sinkronizatutako fitxak @@ -893,16 +906,22 @@ Desaktibatuta - + Baimendu audioa eta bideoa + + Baimendu audioa eta bideoa Blokeatu audioa eta bideoa datu mugikorretan soilik Audioa eta bideoa WiFi bidez erreproduzituko dira soilik - + Blokeatu audioa soilik - + + Blokeatu audioa soilik + Blokeatu audioa eta bideoa + + Blokeatu audioa eta bideoa Aktibatuta diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index b0fb3425e..59b9d908c 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -128,6 +128,8 @@ ویرایش نشانک افزونه‌ها + + افزونه‌ها هیچ افزودنی در اینجا وجود ندارد @@ -212,6 +214,11 @@ بیشتر بدانید + + جست‌وجو در %s + + مستقیماً از نوار آدرس جستجو کنید + بازکردن یک زبانه فایرفاکس جدید @@ -522,6 +529,10 @@ نشانک‌‌های دیگر تاریخچه + + زبانهٔ جدید + + پیدا کردن در صفحه زبانه‌های همگام‌سازی شده @@ -874,16 +885,22 @@ روشن خاموش - + اجازه دادن صوت و ویدئو + + اجازه دادن به صدا و ویدئو مسدود کردن صدا و تصویر فقط بر روی حالت داده صدا و تصویر بر روی حالت Wi-Fi پخش خواهد شد - + مسدود کردن فقط صدا - + + فقط مسدود کردن صدا + مسدود کردن صوت و ویدئو + + مسدود کردن صوت و ویدئو روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index d672e5818..2fd2f384c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -183,6 +183,8 @@ Ulkoasu + + Mukauta lukunäkymää Yhteyden muodostaminen epäonnistui. URL-skeeman tunnistaminen ei onnistu. @@ -897,16 +899,22 @@ Pois - + Salli ääni ja video + + Salli ääni ja video Estä ääni ja video vain käytettäessä matkapuhelinverkon datayhteyttä Ääni ja video toistetaan käytettäessä Wi-Fi-yhteyttä - + Estä vain ääni - + + Estä vain ääni + Estä ääni ja video + + Estä ääni ja video Päällä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 808ade139..c8ed1d932 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -131,6 +131,8 @@ Modifier le marque-page Modules complémentaires + + Extensions Aucun module ici @@ -181,6 +183,8 @@ Apparence + + Personnaliser le mode lecture Impossible de se connecter. Schéma d’URL inconnu. @@ -218,6 +222,11 @@ En savoir plus + + Recherche %s + + Recherchez directement depuis la barre d’adresse + Ouvrir un nouvel onglet dans Firefox @@ -537,6 +546,10 @@ Autres marque-pages Historique + + Nouvel onglet + + Rechercher dans la page Onglets synchronisés @@ -893,16 +906,22 @@ Désactivée - + Autoriser l’audio et la vidéo + + Autoriser l’audio et la vidéo Bloquer l’audio et la vidéo depuis les données mobiles uniquement L’audio et la vidéo seront lus avec une connexion Wi-Fi - + Bloquer l’audio uniquement - + + Bloquer l’audio uniquement + Bloquer l’audio et la vidéo + + Bloquer l’audio et la vidéo Activé diff --git a/app/src/main/res/values-fy-rNL/strings.xml b/app/src/main/res/values-fy-rNL/strings.xml index 4f33c21e0..928a8da77 100644 --- a/app/src/main/res/values-fy-rNL/strings.xml +++ b/app/src/main/res/values-fy-rNL/strings.xml @@ -183,6 +183,8 @@ Uterlik + + Lêzerwerjefte oanpasse Net yn steat te ferbinen. Unkenbere URL-skema. @@ -219,6 +221,12 @@ Mear ynfo + + %s trochsykje + + + Streekrjocht fan de adresbalke út sykje + In nij Firefox-ljepblêd iepenje @@ -884,16 +892,22 @@ Oan Ut - + Audio en fideo tastean + + Audio en fideo tastean Audio en fideo allinnich op mobile data blokkearje Audio en fideo wurde fia wifi ôfspile - + Allinnich audio blokkearje - + + Allinnich audio blokkearje + Audio en fideo blokkearje + + Audio en fideo blokkearje Oan diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 6cf7846f2..1d2e1c835 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -127,6 +127,8 @@ Deasaich an comharra-lìn Tuilleadain + + Leudachain Chan eil tuilleadan sam bith an-seo @@ -522,6 +524,10 @@ Comharran-lìn eile An eachdraidh + + Taba ùr + + Lorg san duilleag Tabaichean sioncronaichte @@ -871,16 +877,22 @@ Air Dheth - + Ceadaich fuaim is video + + Ceadaich fuaim is video Bac an fhuaim agus video air ceangal dàta mobile a-mhàin Gabhaidh fuaim agus video a chluich air WiFi - + Na bac ach an fhuaim - + + Na bac ach an fhuaim + Bach an fhuaim agus a’ video + + Bach an fhuaim agus a’ video Air @@ -1078,6 +1090,8 @@ Saoraidh seo stòras Ceadan nan làrach + + Luchdaidhean a-nuas Sguab an dàta brabhsaidh às @@ -1202,8 +1216,7 @@ Do phrìobhaideachd - Dhealbh sinn %s airson ’s gum bi smachd agad-sa air na cho-roinneas - tu air loidhne agus na cho-roinneas tu leinne. + Dhealbh sinn %s airson ’s gum bi smachd agad-sa air na cho-roinneas tu air loidhne agus na cho-roinneas tu leinn. Leugh sanas na prìobhaideachd againn diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index cd25c2457..875f0fe2a 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -128,6 +128,8 @@ Editar marcador Complementos + + Extensións Aquí non hai complementos @@ -520,6 +522,10 @@ Outros marcadores Historial + + Nova lapela + + Atopar na páxina Lapelas sincronizadas @@ -866,17 +872,23 @@ Activado Desactivado - + Permitir son e vídeo + + Permitir son e vídeo Bloquear son e vídeo só en datos móbiles O son e o vídeo reproduciranse habendo wifi - + Bloquear só o son - + + Bloquear só o son + Bloquear son e vídeo + + Bloquear son e vídeo Activado diff --git a/app/src/main/res/values-gn/strings.xml b/app/src/main/res/values-gn/strings.xml index 66e7e9225..f4f1a7c0a 100644 --- a/app/src/main/res/values-gn/strings.xml +++ b/app/src/main/res/values-gn/strings.xml @@ -184,6 +184,8 @@ Mba’ejegua + + Emboava moñe’ẽhára rechaha Ndaikatúi eike. URL reko ojekuaa’ỹva. @@ -220,6 +222,12 @@ Kuaave + + Eheka %s + + + Eheka kundaharape renda guive + Embojuruja Firefox rendayke pyahu @@ -900,16 +908,22 @@ Hendypyre Mboguepyre - + Emoneĩ mba’epu ha ta’ãngamýi + + Emoneĩ mba’epu ha ta’ãngamýi Ejoko mba’epu ha ta’ãngamýi pumbyry mba’ekuaarãme año Mba’epu ha ta’ãngamýi oñembohetáta Wi-Fi ndive - + Ejoko mba’epu año - + + Ejoko mba’epu año + Ejoko mba’epu ha ta’ãngamýi año + + Ejoko mba’epu ha ta’ãngamýi Hendypyre diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml index 58d511663..5e972d85c 100644 --- a/app/src/main/res/values-hi-rIN/strings.xml +++ b/app/src/main/res/values-hi-rIN/strings.xml @@ -52,7 +52,7 @@ चयनित - %1$s को @fork-maintainers द्वारा निर्मित किया गया है। + %1$s को Mozilla द्वारा निर्मित किया गया है। @@ -472,6 +472,8 @@ अन्य बुकमार्क इतिहास + + नया टैब सिंक हुए टैब @@ -629,6 +631,10 @@ यहां कोई इतिहास नहीं + + खोलें + + माफ़ कीजिए। %1$s उस पृष्ठ को लोड नहीं कर सकता हैं। diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index a2f96eb21..b3c1d73b7 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -61,7 +61,7 @@ Nalaziš se u privatnoj sesiji - %1$s čisti tvoju kronologiju pretraživanja i pregledavanja kad izađeš iz aplikacije ili kad zatvoriš sve kartice i prozore privatnog pregledavanja. Ovo te ne čini anonimnim prema web-stranicama ili tvom pružatelju internet usluga, ali olakšava da zadržiš privatnim ono što radiš na internetu od ostalih osoba koje koriste ovo računalo. + %1$s čisti tvoju povijest pretraživanja i pregledavanja u privatnim karticama kada ih zatvoriš ili izađeš iz aplikacije. Ovo te ne čini anonimnim prema web-stranicama ili tvom pružatelju internetskih usluga, ali olakšava da zadržiš privatnim ono što radiš na internetu od ostalih osoba koje koriste ovaj uređaj. Česti mitovi o privatnom pregledavanju Izbriši sesiju @@ -179,6 +179,8 @@ Izgled + + Prilagodi prikaz za čitanje Nije se moguće povezati. Neprepoznatljiva URL shema. @@ -216,6 +218,11 @@ Saznaj više + + Pretraži %s + + Pretraži izravno iz adresne trake + Otvori novu karticu u Firefoxu @@ -493,9 +500,9 @@ - Svijetli + Svijetla - Tamni + Tamna Postavljeno u postavkama za štednju baterije @@ -892,16 +899,22 @@ Uključeno Isključeno - + Dopusti zvuk i video + + Dopusti zvuk i video Blokiraj zvuk i video samo na mobilnoj vezi Zvuk i video reproducirat će se na Wi-Fi-ju - + Blokiraj samo zvuk - + + Blokiraj samo zvuk + Blokiraj zvuk i video + + Blokiraj zvuk i video Uključeno diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index 366e974d2..b64c370ca 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -181,6 +181,8 @@ Zwonkowny napohlad + + Čitanski napohlad přiměrić Zwisk móžny njeje. Njespóznawajomna URL-šema. @@ -217,6 +219,11 @@ Dalše informacije + + Z %s pytać + + Direktnje z adresoweho pola pytać + Nowy rajtark Firefox wočinić @@ -889,16 +896,22 @@ Zapinjeny Wupinjeny - + Awdio a widejo dowolić + + Awdio a widejo dowolić Awdio a widejo jenož za mobilny zwisk blokować Awdio a widejo so přez WLAN wothrawatej - + Jenož awdio blokować - + + Jenož awdio blokować + Awdio a widejo blokować + + Awdio a widejo blokować Zapinjeny diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 9010518a1..6470877a1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -130,6 +130,8 @@ Könyvjelző szerkesztése Kiegészítők + + Kiegészítők Nincsenek kiegészítők @@ -180,6 +182,8 @@ Megjelenés + + Olvasó nézet testreszabása Nem tud csatlakozni. Felismerhetetlen URL-séma. @@ -217,6 +221,11 @@ További tudnivalók + + %s keresés + + Keresés közvetlenül a címsávból + Új Firefox-lap megnyitása @@ -532,6 +541,10 @@ Más könyvjelzők Előzmények + + Új lap + + Keresés az oldalon Szinkronizált lapok @@ -885,16 +898,22 @@ Ki - + Hang és videó engedélyezése + + Hang és videó engedélyezése Hang és videó blokkolása csak mobil-adatkapcsolaton A hang és videó Wi-Fi-n lesz lejátszva - + Csak a hang blokkolása - + + Csak a hang blokkolása + Hang és videó blokkolása + + Hang és videó blokkolása Be diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 7644c30a0..e62ef605e 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -132,6 +132,8 @@ Edit markah Pengaya + + Ekstensi Tidak ada pengaya di sini @@ -540,6 +542,10 @@ Markah Lain Riwayat + + Tab baru + + Temukan di laman Tab yang disinkronkan @@ -895,16 +901,22 @@ Nonaktif - + Izinkan audio dan video + + Izinkan audio dan video Blokir suara dan video hanya pada mode data seluler Suara dan video akan diputar dalam mode Wi-Fi - + Blokir suara saja - + + Blokir audio saja + Blokir suara dan video + + Blokir audio dan video Aktif diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 0c0d4252d..4192e89a7 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -183,6 +183,8 @@ Aspetto + + Personalizza modalità lettura Connessione non riuscita. Schema URL non riconoscibile. @@ -220,6 +222,11 @@ Ulteriori informazioni + + Cerca in %s + + Cerca direttamente dalla barra degli indirizzi + Apri una nuova scheda in Firefox @@ -903,16 +910,22 @@ Disattivata - + Consenti audio e video + + Consenti audio e video Blocca audio e video solo con connessione dati Audio e video saranno riprodotti con una connessione Wi-Fi - + Blocca solo l’audio - + + Blocca solo l’audio + Blocca audio e video + + Blocca audio e video Attivato diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 1898fcb8b..d59d78e2d 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -180,6 +180,8 @@ תצוגה + + התאמה אישית של תצוגת קריאה לא ניתן להתחבר. מבנה הכתובת אינו ברור. @@ -216,6 +218,11 @@ מידע נוסף + + חיפוש ב־%s + + חיפוש ישירות משורת הכתובת + פתיחת לשונית חדשה ב־Firefox @@ -495,6 +502,8 @@ בהיר כהה + + הוגדר על־ידי מצב חיסכון בסוללה שימוש בערכת הנושא של המכשיר @@ -616,6 +625,8 @@ סגירת כל הלשוניות לשונית חדשה + + הביתה החלפת מצב לשונית @@ -883,16 +894,22 @@ כבויה - + לאפשר שמע ווידאו + + לאפשר שמע ווידאו לחסום שמע ווידאו בחיבור סלולרי בלבד שמע ווידאו יופעלו על גבי Wi-Fi - + לחסום שמע בלבד - + + לחסום שמע בלבד + לחסום שמע ווידאו + + לחסום שמע ווידאו פעיל @@ -1467,6 +1484,8 @@ שם משתמש ססמה + + להכניס את הקוד שלך מחדש יש לבטל את הנעילה כדי להציג את הכניסות השמורות שלך @@ -1505,12 +1524,18 @@ יש לבטל את הנעילה כדי להציג את הכניסות השמורות שלך שמירה מאובטחת של הכניסות והססמאות שלך + + ניתן להגדיר תבנית נעילת מכשיר, קוד או ססמה כדי להגן על פרטי הגישה והססמאות שלך מפני גורמים בלתי מהימנים שמחזיקים במכשיר שלך. מאוחר יותר + + להגדיר כעת שחרור נעילת המכשיר שלך + + התמקדות על כל האתרים יש להפעיל כדי לאפשר צביטה והתקרבות, אפילו באתרים שמונעים את תנועות האצבעות האלו. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index a757700b2..06a2c93e2 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -133,6 +133,8 @@ ブックマークを編集 アドオン + + 拡張機能 アドオンがありません @@ -183,6 +185,8 @@ 外観 + + リーダービューをカスタマイズ 接続できません。認識できない URL スキームです。 @@ -220,6 +224,11 @@ 詳細情報 + + %s で検索 + + アドレスバーから直接検索します + 新しい Firefox タブを開く @@ -537,6 +546,10 @@ 他のブックマーク 履歴 + + 新しいタブ + + ページ内検索 同期したタブ @@ -895,16 +908,22 @@ オフ - + 音声と動画の再生を許可 + + 音声と動画の再生を許可 データ通信時のみ音声と動画をブロック Wi-Fi 接続時は音声と動画を再生します - + 音声のみブロック - + + 音声のみブロック + 音声と動画をブロック + + 音声と動画をブロック オン diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 336ff332e..572aa547f 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -127,6 +127,8 @@ სანიშნის ჩასწორება დამატებები + + გაფართოებები დამატებები არ არის @@ -175,6 +177,8 @@ იერსახე + + კითხვის რეჟიმის მორგება ვერ დაუკავშირდა. გაურკვეველი URL-სქემა. @@ -211,6 +215,11 @@ ვრცლად + + საძიებოდ %s + + მოიძიეთ პირდაპირ მისამართების ველიდან + ახალი Firefox-ჩანართის გახსნა @@ -526,6 +535,10 @@ სხვა სანიშნები ისტორია + + ახალი ჩანართი + + პოვნა გვერდზე დასინქრონებული ჩანართები @@ -879,16 +892,22 @@ ჩართ. გამორთ. - + ხმისა და ვიდეოს დაშვება + + ხმისა და ვიდეოს დაშვება ხმისა და ვიდეოს შეზღუდვა, მხოლოდ ფიჭურ ინტერნეტზე ხმისა და ვიდეოს დაშვება Wi-Fi-ზე - + მხოლოდ ხმის შეზღუდვა - + + მხოლოდ ხმის შეზღუდვა + ხმისა და ვიდეოს შეზღუდვა + + ხმისა და ვიდეოს შეზღუდვა ჩართ. @@ -955,7 +974,7 @@ ბოლოს გამოყენებული - Sync შესვლა + დასინქრონებაში შესვლა ყველა მოწყობილობაზე გაგზავნა diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 7018fa864..98598bd12 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -183,6 +183,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Arwes + + Sagen timeẓri n tɣuri Ur izmir ara ad yeqqen. Azenziɣ n tensa URL ur yettwassen ara. @@ -219,6 +221,11 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Issin ugar + + Nadi %s + + Nadi srid seg ufeggag n tansiwin + Ldi iccer amaynut n Firefox @@ -898,16 +905,22 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara Irmed Insa - + Sireg ameslaw d uvidyu + + Sireg ameslaw d tvidyut Sewḥel imeslawen d tvidyutin ded yisefka izirazen kan Imeslawen d tividyutin ad uraren deg WI-FI - + Sewḥel kan imeslawen - + + Sewḥel kan ameslaw + Sewḥel imeslawen d tvidyutin + + Sewḥel ameslaw d tvidyut Irmed diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml index d7944dc43..a51ebc93b 100644 --- a/app/src/main/res/values-kk/strings.xml +++ b/app/src/main/res/values-kk/strings.xml @@ -127,6 +127,8 @@ Бетбелгіні түзету Қосымшалар + + Кеңейтулер Осында қосымшалар жоқ @@ -174,6 +176,8 @@ Қолданбада ашу Сыртқы түрі + + Оқу режимін баптау Байланысу мүмкін емес. URL схемасы белгісіз. @@ -211,6 +215,11 @@ Көбірек білу + + %s ішінен іздеу + + Адрестік жолақтан тікелей іздеу + Жаңа Firefox бетін ашу @@ -520,6 +529,10 @@ Басқа бетбелгілер Шолу тарихы + + Жаңа бет + + Беттен табу Синхрондалған беттер @@ -870,16 +883,22 @@ Іске қосулы Сөндірулі - + Аудио мен видеоны рұқсат ету + + Аудио мен видеоны рұқсат ету Аудио мен видеоны тек ұялы деректерде бұғаттау Аудио және видео Wi-Fi арқылы ойнатылады - + Тек аудионы бұғаттау - + + Тек аудионы бұғаттау + Аудио мен видеоны бұғаттау + + Аудио мен видеоны бұғаттау Іске қосулы diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml index a883ba0e3..d7908dd91 100644 --- a/app/src/main/res/values-kmr/strings.xml +++ b/app/src/main/res/values-kmr/strings.xml @@ -14,7 +14,7 @@ Gerîna veşartî bigire - Lêgerîn yan navnîşanek + Lêgerîn an jî navnîşan Hilpekînên te yên vekirî ew ê li vir bên nîşandan. @@ -36,19 +36,19 @@ Koleksiyonê hilbijêre - Ji moda pir-bijartinê derkeve + Ji moda bijartina-pir derkeve Hilpekînên bijartî li koleksiyonê qeyd bike %1$s hat bijartin - Bıjartina %1$s hate rakirin + Bijartina %1$s hate rakirin - Ji moda pir-bijartinê hate derketin + Ji moda bijartina-pir hate derketin - Tu ket moda pir-bijartinê, ji bo tomarkirina li koleksiyonê hilpekînan hilbijêre + Tu ket moda bijartina-pir, ji bo tomarkirina li koleksiyonê hilpekînan hilbijêre Hate bijartin @@ -61,9 +61,9 @@ - %1$s, dema ku te hilpekînên veşartî girtin an jî tu ji sepanê derket ew ê raboriya te ya gerîn û lêgerînê ji hilpekînên veşartî bên paqijkirin. Ev, rê li ber şopandina/nedîtina te ya ji aliyê malper û peydakerên servîsê ve nagirê lê heke hin kesên din ên vê cîhazê bi kar tînin hebin ew ê bihêle ku tu raboriya xwe ji wan veşêrî. + %1$s, dema ku te hilpekînên veşartî girtin an jî tu ji sepanê derket ew ê raboriya te ya gerîn û lêgerînê ji hilpekînên veşartî bên paqijkirin. Ev, rê li ber şopandina te ya ji aliyê malper û peydakerên servîsê ve nagirê lê heke hin kesên din ên vê cîhazê bi kar tînin hebin ew ê bihêle ku tu raboriya xwe ji wan veşêrî. - Efsaneyên berbelav ên derbarê gerîna veşartî de + Efsaneyên berbelav ên têkildarî gerîna veşartî Rûniştinê jê bibe @@ -85,7 +85,7 @@ Bigire - Gihîna kamerayê hewce ye. Here sazkariyên Androidê ji beşa destûran, destûrê bide. + Gihîna li kamerayê hewce ye. Here sazkariyên Androidê ji beşa destûran, destûrê bide. Here sazkariyan @@ -99,7 +99,7 @@ Bigire - Tu dikarî raxistina hilpekînên vekirî biguherînî. Here sazkariyan û ji bin "xuyabûna hilpekînê" gridê hilbijêre. + Tu dikarî raxistina hilpekînên vekirî biguherînî. Here sazkariyan û ji bin "xuyabûna hilpekînan" grîdê hilbijêre. Here sazkariyan @@ -126,11 +126,13 @@ Rawestîne - Favorî + Tevlî favoriyan bike - Favoriyan serast bike + Favoriyan sererast bike Pêvek + + Pêvek Ti pêvek li vir tune @@ -196,13 +198,13 @@ - Sken + Kodê bixwîne Motora lêgerînê Sazkariyên motora lêgerînê - Îja ka bi vê bigere: + Îja ka bi vê lê bigere: Girêdanê ji panoyê bîne @@ -210,9 +212,9 @@ Destûrê nede - Di rûniştina veşartî de bila pêşniyarên lêgerînê werin pêş te? + Bila pêşniyarên lêgerînê di rûniştina veşartî de werin pêş te? - %s dê hemû tiştên tu li darikê navnîşanan binivîsî bi motora te ya lêgerînê ya derbasdar re parve bike. + %s dê hemû tiştên tu li darika navnîşanan binivîsî bi motora te ya lêgerînê ya derbasdar re parve bike. Zêdetir bizane @@ -240,7 +242,7 @@ Lêgerîn - Dariikê navnîşanan + Darika navnîşanê Alîkarî @@ -249,7 +251,7 @@ Paşragihandinê bişîne - Derbarê %1$s + Derbarê %1$s de @@ -273,19 +275,19 @@ Girêdanan di hilpekîna veşartî de veke - Destûrê bide girtina wêneyê ekranê di gera nepen de + Di gerîna veşartî de, bihêle bila wêneyê ekranê bê kişandin - Heke destûrê bidî, gava gelek bernamok vekirî bin hilpekînên nepen dê xuya bibin + Heke destûr bê dayîn, dema ku gelek sepan vekirî bin jî ew ê hilpekînên veşartî xuya bibin - Kurteriya gera nepen lê zêde bike + Kurterêya gerîna veşartî tevlî bike Gihînbarî Servera taybet a hesabê Firefoxê - Servera taybet a Sync`ê + Servera Sync’ê ya taybet - Hesabê Firefoxê/PÊşkêşkara Sync hat guhertin. Ji bo sepandina guhertinan ji bernameyê derdikeve… + Servera Sync/Hesabê Firefoxê hat guhertin. Ji bo sepandina guhertinan ji sepanê derdikeve… Hesab @@ -297,15 +299,15 @@ Serrûpel - Îşaretên tiliyan + Hereketên tiliyan Taybet bike - Bi hesabê xwe yê Firefoxê favorî, mêjû û zêdetir tiştên xwe hevseng bike + Bi hesabê xwe yê Firefoxê favorî, raborî û zêdetirî wan senkronîze bike Hesabê Firefoxê - Ji bo dewamkirina hevsengkirinê dîsgirêdanê çêke + Ji bo senkronîzekirinê bidomînî, dîsa girê bide Ziman @@ -317,7 +319,7 @@ Amûrên pêşvebirinê - Bi riya USBê ji dûr ve neqandina çewtiyan + Bi rêya USB’ê ji dûr ve neqandina çewtiyan Motorên lêgerînê nîşan bide @@ -331,9 +333,9 @@ Di raboriya gerînê de lê bigere - Di favoriyan de bigere + Di favoriyan de lê bigere - Di hilpekînên hevsengkirî de bigere + Di hilpekînên senkronîzekirî de lê bigere Sazkariyên hesêb @@ -341,7 +343,7 @@ Girêdanan di sepanan de veke - Rêvebera jêbarkirinê ya derveyî + Rêvebera daxistinê ya derveyî Pêvek @@ -360,15 +362,14 @@ Xwediyê koleksiyonê (nasnameya bikarhêner) - Koleksiyona pêvekan hat guhertin. Ji bo sepandina guhertinan ji bernamokê derdikeve… + Koleksiyona pêvekan hat guhertin. Ji bo sepandina guhertinan ji sepanê derdikeve… Pêvek nayê piştgirîkirin - Pêvek jixwe sazkirî ye - + Pêvek jixwe sazkirî ye @@ -409,13 +410,13 @@ - Hilpekînên hatine wergirtin + Hilpekînên hatine stendin - Danezanên ji bo hilpekînên ji amûrên din yên Firefoxê hatî + Danezanên hilpekînan ên ji cîhazên Firefoxê tên stendin. - Hilpekîn hate wergirtin + Hilpekîn hate stendin - Hilpekîn hatin wergirtin + Hilpekîn hatin stendin Hilpekîna ji %s ve hat @@ -426,7 +427,7 @@ Parastina ji Şopandinê - Naverok û skrîptên li ser înternetê te dişopînin asteng bike + Naverok û skrîptên ku li ser înternetê te dişopînin asteng bike Istisna @@ -434,17 +435,19 @@ Ji bo hemû malperan veke - Istisna dihêle ku tu dikaribî ji bo malperên hilbijartî parastina şopandinê bigirî. + Istisna dihêlin ku tu bikaribî ji bo malperên hilbijartî parastina ji şopandinê bigirî. Zêdetir bizane - Bi temamî hat girtin, ji bo vekirinê here Eyaran. + Bi temamî hat girtin, ji bo vekirinê here Sazkariyan. Telemetrî Daneyên bikaranînê û teknîkî + + Performans, bikaranîn, reqalav û daneyên taybetkirinê yên têkildarî geroka te bi Mozillayê re parve dike, ji bo ku em karibin pê %1$s’ê baştir bikin Daneyên bazarkirinê @@ -468,17 +471,17 @@ Sync’ê veke - Koda hevsengkirinê ya di Firefoxa Sermaseyê de sken bike + Koda hevcotkirinê ya di Firefoxa Sermaseyê de bide xwendin Têkeve - Ji bo dîsberibandinê têkevinê + Ji bo dîsa girê bibî, têkeve Hesêb rake - firefox.com/pair de xuya dibe sken bike]]> + firefox.com/pair de xuya dibe, bide xwendin]]> Kamerayê veke @@ -497,7 +500,7 @@ Tarî - Li gorî teserufa akuyê + Li gorî teserufa bateryayê Rûkara cîhazê bi kar bîne @@ -505,11 +508,11 @@ Ji bo nûkirinê bikişîne - Ji bo veşartina derikê amûran bişemitîne + Ji bo darikê amûran veşêrî, bişemitîne - Ji bo guhertina hilpekînê darikê amûran bi kêlekê ve kaş bike + Ji bo hilpekînê biguherînî, darikê amûran biherikîne kêlekê - Ji bo vekirina hilpekînan darikê amûran bi jorê ve bikişîne + Ji bo hilpekînan vekî, darikê amûran biherikîne jorê @@ -523,15 +526,19 @@ Favoriyên Sermaseyê - Menuya Favoriyan + Menûya Favoriyan - Darikê Amûran yê Favoriyan + Darikê Amûran ê Favoriyan Favoriyên din Raborî + + Hilpekîna nû + + Di rûpelê de bibîne - Hilpekînên hevsengkirî + Hilpekînên senkronîzekirî Lîsteya xwendinê @@ -544,9 +551,9 @@ Bigire - Hilpekînên dawiyê hatî girtin + Hilpekînên herî dawî hatine girtin - Mêjûyê hemû nîşan bide + Raboriyê gişî nişan bide %d hilpekîn @@ -555,17 +562,17 @@ %d hilpekîn - Hilpekîna di nêzîk de hatî girtin nîn in + Hilpekîna berî niha hatiye girtin tune Hilpekîn - Xuyabûna hilpekînê + Xuyabûna hilpekînan Lîste - Grid + Grîd Hilpekînan bigire @@ -609,7 +616,7 @@ Hemû hilpekînan parve bike - Hilpekînên dawiyê hatî girtin + Hilpekînên herî dawî hatine girtin Sazkariyên hilpekînê @@ -627,7 +634,7 @@ Hilpekînên hilbijartî parve bike - Menuya hilpekînên hilbijartî + Menûya hilpekînên hilbijartî Hilpekînê ji koleksiyê rake @@ -691,7 +698,7 @@ Paqij bike - Kopî + Kopî bike Parve bike @@ -762,9 +769,9 @@ - Menuya Favoriyan + Menûya favoriyan - Favoriyan serast bike + Favoriyan sererast bike Peldankê hilbijêre @@ -802,11 +809,11 @@ The first parameter is the number of bookmarks selected --> %1$d hatin bijartin - Favoriyan serast bike + Favoriyan sererast bike Peldankê sererast bike - Ji bo favoriyan hevsengirî bibînî têkevê + Ji bo favoriyên senkronêzekirî bibînî, têkeve NAVNÎŞAN @@ -842,7 +849,7 @@ - Rûpela sazkariyên zû + Rûpela sazkariyên bilez Tê pêşniyarkirin @@ -872,7 +879,7 @@ Hate astengkirin - Destûrgirtî + Destûr hat dayîn Ji hêla Androidê ve hate astengkirin @@ -881,16 +888,22 @@ Vekirî Girtî - + Destûrê bide deng û vîdyoyê + + Destûrê bide deng û vîdyoyê Deng û vîdyoyê tenê di daneya hucreyî de asteng bike Deng û vîdyo ew ê tenê di Wi-Fi de bên vekirin - + Tenê dengan asteng bike - + + Tenê dengan asteng bike + Deng û vîdyoyê asteng bike + + Deng û vîdyoyê asteng bike Vekirî @@ -902,7 +915,7 @@ Menûya koleksiyonê - Tiştên ji te re girîng berhev bike.\nLêgerîn, malper û hilpekînên dixwazî xwe zû bigihînî wan bîne cem hev. + Tiştên ku ji te re girîng in berhev bike.\nLêgerîn, malper û hilpekînên ku tu dixwazî xwe zû bigihînî wan bîne cem hev. Hilpekînan hilbijêre @@ -954,7 +967,7 @@ Hemû çalakî - Dawiyê hatî bikaranîn + Bikaranînên herî dawî Têkeve Sync’ê @@ -966,29 +979,29 @@ Cîhazeke din girê bide - Ji bo şandina hilpekînekê herî kêm ji amûreke din têkeve Firefoxê. + Ji bo şandina hilpekînekê, herî kêm ji cîhazeke din têkeve Firefoxê. Min fêm kir - Bi vê amûrê re nayê parvekirin + Bi vê sepanê re nayê parvekirin Bişîne cîhazê - Amûra girêdayî nîn e + Cîhaza girêdayî tune - Derbarê Şandina Hilpekînan de agahiyan bistîne… + Derbarê şandina hilpekînan de agahiyan bistîne… - Bi amûreke din ve girê bide… + Cîhazeke din girê bide… - Danişîna gera nepen + Rûniştina gerîna veşartî - Hilpekînên nepen jê bibe + Hilpekînên veşartî jê bibe - Hilpekînên nepen bigire + Hilpekînên veşartî bigire Veke @@ -1031,7 +1044,7 @@ Bipejirîne - Destûrê bide %1$sê ku %2$sê veke + Destûrê bide %1$s’ê ku %2$s’ê veke BIHÊLE @@ -1039,7 +1052,7 @@ Tu bi rastî jî dixwazî koleksiyana %1$s’ê were jêbirin? - Jêbirina vê hilpekînê dê hemû koleksiyê jê bibe. Kengê bixwazî dikarî koleksiyoneke nû biafirînî. + Jêbirina vê hilpekînê dê hemû koleksiyonê jê bibe. Kengî bixwazî dikarî koleksiyoneke nû biafirînî. %1$s’ê jê bibe? @@ -1051,16 +1064,16 @@ URL hate kopîkirin - Ev nivîseke nimûneyî ye. Li virê xuya dibe ka gava tu bi vê sazkariyê mezinahiyê biguherînî dê çawa be. + Ev, nivîsa nimûne ye. Dema ku tu bi vê eyarê mezinbûnê biguherînî dê çawaniya nivîsê li vir xuya bibe. - Nivîsên li ser malperan mezintir yan biçûktir bike + Nivîsên li ser malperan mezintir an jî biçûktir bike Mezinahiya fontê - Lêaîna mezinahiya nivîsan ya xweber + Mezinkirina fontê ya bixweber - Mezinahiya nivîsan dê li gorî sazkariyên Androîda te be. Ji bo mezinahiyê nivîsê li virê biguherînî vê wê betal bike. + Mezinahiya fontê ew ê li gorî sazkariyên Androida te be. Ji bo mezinahiya fontê li vir biguherînî, vê eyarê betal bike. Daneyên gerînê jê bibe @@ -1081,11 +1094,11 @@ Kûkî - Danişînên li ser gelek malperan dê bên girtin + Têketinên te yên li ser gelek malperan dê bên girtin - Wêne û pelên di pêşbîrê de + Wêne û dosyeyên di pêşbîrê de - Ciyê depokirinê vala dike + Qada depokirinê vala dike Destûrên malperan @@ -1093,25 +1106,25 @@ Daneyên gerînê jê bibe - Daneyên gerê di dema derketinê de jê bibe + Di derketinê de daneyên gerînê jê bibe - Gava tu ji menuya giştî "Derkeve"yê hilbijêrî daneyên gerê xweber paqij dike + Gava tu ji menûya giştî "Derkeve"yê hilbijêrî daneyên gerînê xweber paqij dike - Gava tu ji menuya giştî \"Derkeve\"yê hilbijêrî daneyên gerê xweber paqij dike + Gava tu ji menûya giştî \"Derkeve\"yê hilbijêrî daneyên gerînê xweber paqij dike Derkeve - Ev ê hemû daneyên te yên gerê jê bibe + Ev ê hemû daneyên te yên gerînê jê bibe - %s dê daneyên gerê yên hilbijartî jê bibe. + %s dê daneyên gerînê yên hilbijartî jê bibe. Betal bike Jê bibe - Daneyên gerînê hate jêbirin + Daneyên gerînê hatin jêbirin Daneyên gerînê tên jêbirin… @@ -1121,28 +1134,28 @@ - Firefox Nightly her şev tê nûkirin û tê de taybetiyên nû yên ceribandinê hene. - Lê belê, dibe ku hindiktir stabîl be. Ji bo ceribandineke stabîltir geroka me ya beta jêbar bike. + Firefox Nightly her şev tê nûvekirin û tê de taybetiyên nû yên ceribandinê hene. + Lê belê, dibe ku hindiktir stabîl be. Ji bo ceribandineke stabîltir geroka me ya betayê daxîne. - Firefox Betaya ji bo Andoîdê jêbar bikin + Betaya Firefoxê ji bo Androidê daxîne - Firefox Nightlyê bar kir + Firefox Nightly hat veguhastin - Ev sepan dê venûkirinên ewlekariyê êdî wernegire. Êdî vê sepanê bi kar neyne û derbasî Nightlya nû bibe. - \ n \ nJi bo transferkirina favorî, têketin û mêjûya xwe bo sepaneke din, hesabekî Firefoxê biafirîne. + Ev sepan ew ê êdî nûvekirinên ewlekariyê wernegire. Êdî vê sepanê bi kar neyîne û derbasî Nightlya nû bibe. + \ n \ nJi bo transferkirina favorî, hesab û raboriya xwe li sepaneke din, hesabekî Firefoxê biafirîne. Derbasî Nightlya nû bibe - Firefox Nightlyê bar kir + Firefox Nightly hat veguhastin - Ev sepan dê venûkirinên ewlekariyê êdî wernegire. Êdî vê sepanê bi kar neyne û derbasî Nightlya nû bibe. - \ n \ nJi bo transferkirina favorî, têketin û mêjûya xwe bo sepaneke din, hesabekî Firefoxê biafirîne. + Ev sepan ew ê êdî nûvekirinên ewlekariyê wernegire. Êdî vê sepanê bi kar neyîne û derbasî Nightlya nû bibe. + \ n \ nJi bo transferkirina favorî, hesab û raboriya xwe li sepaneke din, hesabekî Firefoxê biafirîne. - Nightlya nû jêbar bike + Nightlya nû daxîne - Pirsên te derbarê %sa nû de hene? Dixwazî bizanî ka çi guheriye? + Pirsên te derbarê %s’ê ya nû de hene? Dixwazî bizanî ka çi guheriye? Bersiv li vir in - Bi hesabê xwe yê Firefoxê dest bi hevsengkirina favoriyan, şîfreyan û zêdetir tiştên xwe bike. + Bi hesabê xwe yê Firefoxê dest bi senkronîzekirina favoriyan, pêborînan û zêdetirî wan, bike. Zêdetir bizane - Te li ser vê amûrê wekî %s li ser gerokeke din a Firefoxê têketin pêk anî. Tu dixwazî bi vî hesabî têkeviyê? + Tu li ser vê cîhazê di gerokeke din a Firefoxê de wekî %s têketî yê. Tu dixwazî bi vî hesabî têkevî? Erê, têkeve @@ -1184,30 +1197,30 @@ Nihêniya otomatîk - Sazkariyên nepeniyê û ewlekariyê şopîneran û sepanên niyetxirab û şirketên dixwazin te bişopînin asteng dike. + Sazkariyên nihênî û ewlekariyê şopîneran û sepanên niyetxerab û şirketênku dixwazin te bişopînin asteng dike. Standard (jixweber) - Hindiktir şopîneran asteng dike. Rûpel dê bi awayê asayî bên barkirin. + Hindiktir şopîneran asteng dike. Rûpel ew ê bi awayê normal vebin. Tund (tê pêşniyarkirin) Tund - Zêdetir şopîneran, reklaman û popupan asteng dike. Rûpel zûtir vedibin, lê dibe ku hin taybetî nexebitin. + Zêdetir şopîneran, reklaman û pencereyên vebûnok asteng dike. Rûpel zûtir vedibin, lê dibe ku hin taybetî nexebitin. Aliyê xwe hilbijêre - Bi darikê amûran yê li xwarê gera bi destekî biceribîne yan jî wê bibe jorê. + Darikê amûran ê jêrîn bi destekî biceribîne an jî wê bibe jorê tu bi kêfa xwe yî. - Bi nepenî bigere + Bi nihênî bigere - Carekê hilpekîneke nepen veke: Pêl îkona %sê bike. + Ji bo carekê hilpekîneke veşartî veke: Li îkona %s’ê bitikîne. - Her car hilpekînên nepen veke: Sazkariyên xwe yên gera nepen venû bike. + Her carê hilpekînên veşartî veke: Sazkariyên xwe yên gerîna veşartî nûve bike. Sazkariyan veke @@ -1227,11 +1240,11 @@ Rûkara xwe hilbijêre - Bi çalakkirina moda tarî him ji bataryayê teseruf bike him çavên xwe vehesîne. + Bi çalakkirina moda tarî him ji bataryayê teserûf bike him jî çavên xwe rehet bike. Otomatîk - Xwe li sazkariyên amûra te tîne + Xwe li sazkariyên cîhaza te adabte dike Rûkara tarî @@ -1246,71 +1259,71 @@ DÎSA BICERIBÎNE - Kodê sken bike + Kodê bide xwendin - https://firefox.com/pair]]> + https://firefox.com/pair]]> - Ji bo skenkirinê amade me + Aniha kodê bide xwendin - Bi kameraya xwe têkevê + Bi kameraya xwe têkeve - Bi e-peyamê têkevê + Bi emaîlê têkeve - Yekê biafirîne ku Firefoxê di navbera amûran de hevseng bikî.]]> + Hesêb veke.]]> - Firefox dê hevsengkirina bi hesabê te re asteng bike, lê dê daneyên te yên gerê yên li ser vê amûrê jê nebe. + Firefox dê bi hesabê te re senkronîzekirinê rawestîne, lê ti daneyên te yên gerînê yên di vê cîhazê de nayên jêbirin. - %s dê hevsengkirina bi hesabê te re asteng bike, lê dê daneyên te yên gerê yên li ser vê amûrê jê nebe. + %s ew ê bi hesabê te re senkronîzekirinê rawestîne, lê ti daneyên te yên gerînê yên di vê cîhazê de nayên jêbirin. Girêdanê qut bike Betal bike - Peldankên standard nayên serastkirin + Peldankên standard nayên sererastkirin Sazkariyên parastinê - Parastina şopandinê ya pêşketî + Parastina ji şopandinê ya pêşketî - Bêyî ku bêtî şopandin bigere + Bêyî ku tu werî şopandin bigere - Bila daneyên te ji te re bimînin. %s te ji piraniya şopîneran diparêze ku tiştên tu li ser înternetê dikî dişopînin. + Bila daneyên te ji te re bimînin. %s te ji gelek şopdarên dilxerab diparêze û nahêle tiştên ku tu li ser înternetê dikî ji aliyê wan ve bên dîtin. Zêdetir bizane Standard (heyî) - Hindiktir şopîneran asteng bike. Rûpel dê asayî vebin. + Hindiktir şopîneran asteng dike. Rûpel ew ê bi awayê normal vebin. - Bi parastina şopandinê ya standard çi tên astengkirin? + Bi parastina ji şopandinê ya standard çi tên astengkirin? Tund - Zêdetir şopîneran, reklaman û popupan asteng dike. Rûpel zûtir tên barkirin, lê dibe ku hin taybetî nexebitin. + Zêdetir şopîneran, reklaman û pencereyên vebûnok asteng dike. Rûpel zûtir vedibin, lê dibe ku hin taybetî nexebitin. - Bi parastina şopandinê ya tund çi tên astengkirin? + Bi parastina ji şopandinê ya tund çi tên astengkirin? Taybet - Hilbijêre ka kîjan şopîner û skrîpt bên astengkirin. + Hilbijêre ka kîjan şopdar û skrîpt ew ê bêne astengkirin. - Bi parastina şopandina taybet çi tên astengkirin? + Bi parastina ji şopandinê ya taybet çi tên astengkirin? Kûkî Şopînerên nav-malperî û şopînerên medyaya civakî - Çerezên malperên nehatine bikaranîn + Çerezên ji malperên ku nehatine ziyaretkirin - Hemû çerezên partiya sêyemîn (dibe ku hin malper xira bibin) + Hemû çerezên partiya sêyemîn (dibe ku hin malper xera bibin) - Hemû çerezan (dibe ku hin malper xira bibin) + Hemû çerez (dibe ku hin malper xera bibin) - Naverokên şopandinê + Naverokên ji bo şopandinê Di hemû hilpekînan de @@ -1321,7 +1334,7 @@ Kankerên krîpto Berhevkarên şoptiliyan - Astengker + Yên astengbûyî Destûrgirtî @@ -1341,32 +1354,30 @@ Naverokên şopandinê - Barkirina reklam, vidyo û naverokên din yên tê de koda şopandin heye asteng dike. Dibe ku bandorê li taybetiyên hin malperan bike. + Nahêle reklam, vîdyo û naverokên din ên tê de koda şopandinê hene bên barkirin. Dibe ku bandorê li taybetiyên hin malperan bike. - Gava mertal bû rengê mor, nexwe %sê şopîner asteng kirine. Ji bo agahiyên zêdetir bipelîne. + Gava mertal bû rengê mor, tê vê wateyê ku %s’ê şopîner asteng kirine. Ji bo agahiyên zêdetir, bitepîne. - Parastin ji bo v malperê VEKIRÎ ne. + Parastin ji bo vê malperê VEKIRÎ ne. Parastin ji bo vê malperê GIRTÎ ne - Parastina şopandinê ya pêşketî ji bo van malperan hat girtin + Parastina ji şopandinê ya pêşketî ji bo van malperan hat girtin Paşve here Mafên te - Pirtûkxaneyên çavkaniya azad yên em bi kar tînin + Pirtûkxaneyên çavkaniya azad ên em bi kar tînin - Di %s çi tiştên nû hene? + Di %s’ê de çi tiştên nû hene? %s | Pirtûkxaneyên OSS’ê Şoînerên beralîkirinê - - Çerezên aîdî beralîkirinên ji bo malperên şopandinê yên naskirî asteng dike. Piştgirî @@ -1380,12 +1391,12 @@ Agahiyên lîsanskirinê - Pirtûkxaneyên em bikar tînin + Pirtûkxaneyên em bi kar tînin - Menuya neqandina çewtiyan: Ji bo çalakkirinê %1$d tikandin man + Menûya neqandina çewtiyan: Ji bo çalakkirinê %1$d tikandin man - Menuya neqandina çewtiyan hat çalakkirin + Menûya neqandina çewtiyan çalek e 1 hilpekîn @@ -1414,7 +1425,7 @@ Navê kurterêyê - Tu dikarî vê malperê bi hêsanî li ekrana Serûpela amûra xwe zêde bikî ku zû bigihiyê û wê wekî sepanekê bi kar bînî. + Tu dikarî vê malperê bi hêsanî li ekrana Destpêkê ya cîhaza xwe zêde bikî ku ji wir zûtir bigihîjî wê û wê wekî sepanekê bi kar bînî. Hesab û pêborîn @@ -1439,15 +1450,15 @@ Hesabên tomarkirî - Têketinên te tomar kirine yan hevsengkirinên bi %sê re dê li virê bên nîşandan. + Hesabên ku te tomar kirine yan jî te bi %s’ê re senkronîze kirine dê li vir bên nîşandan. Derbarê Sync’ê de agahî bistîne. Istisna - Têketin û şîfreyên nehatine tomarkirin dê li virê bên nîşandan. + Hesab û pêborînên nehatine tomarkirin dê li vir bên nîşandan. - Têketin û şîfre dê ji bo van malperan neyên tomarkirin. + Hesab û pêborîn dê ji bo van malperan neyên tomarkirin. Hemû istisnayan jê bibe @@ -1455,7 +1466,7 @@ Alfabetîkî - Dawiyê hatî bikaranîn + Bikaranînên herî dawî Malper @@ -1465,9 +1476,9 @@ PIN’a xwe dîsa binivîse - Ji bo têketinên xwe yên tomarkirî bibînî kilîdê veke + Ji bo hesabên xwe yên tomarkirî bibînî, kilîdê veke - Ev girêdan ne ewle ye. Îhtîmal heye têketinên li virê bên bidestxistin. + Ev girêdan ne ewle ye. Îhtîmal heye agahiyên hesêb ên li vir hatine nivîsîn bên bidestxistin. Zêdetir bizane @@ -1516,7 +1527,7 @@ Bikaranîna dawî - Menuya rêzkirina têketinan + Menûya rêzkirina hesaban Motora lêgerînê tevlî bike @@ -1537,9 +1548,9 @@ Nav - Rêzeya lêgerînê ya ji bo bikaranînê + Rêzika lêgerînê ya ji bo bikaranînê - Daxwazê bi “%s”. Mînak:\nhttps://www.google.com/search?q=%sê biguherîne + Lêpirsînê bi “%s”ê pev biguherîne. Mînak:\nhttps://www.google.com/search?q=%s Zêdetir Bizane @@ -1553,11 +1564,11 @@ Motora lêgerînê ya bi navê “%s” jixwe heye. - Rêzikeek lêgerînê binivîse + Rêzikeke lêgerînê binivîse - Kontrol bike ka rêzika lêgerînê û formata Mînak li hev dikin yan na + Kontrol bike ka rêzika lêgerînê û formata nimûne li hev dikin an na - Êewtiya girêdana bi “%s”ê ve + Li “%s”ê ve nehate girêdan %s hat afirandin @@ -1566,13 +1577,13 @@ %s hat jêbirin - Bi xêr hatî geroka %sê ya nipînû + Bi xêr hatî geroka %s’ê ya nipînû Me geroka te bi performans û taybetiyên pêşxistî ji serî heta binî nû kir ku tu dikarî li ser înternetê tiştên zêdetir bikî.\n\nTika ye heta %sê van venû bike li bendê bin... %s tê nûvekirin… - %sê bide destpêkirin + %s’ê bide destpêkirin Veguhestin qediya @@ -1583,24 +1594,24 @@ 1. Here sazkariyên Androidê - Destûrê bitikîne]]> + Destûr’ê bitikîne]]> - %1$sê bike VEKIRÎ]]> + %1$s’yê bike VEKIRÎ]]> Girêdana ewle Girêdana neewle - Tu ji dil dizanî hemû destûrên li ser hemû malperan jê bibî? + Tu ji dil dixwazî hemû destûrên li ser hemû malperan jê bibî? - Tu ji dil dizanî hemû destûran ji bo hemû malperan jê bibî? + Tu ji dil dixwazî hemû destûran ji bo vê malperê jê bibî? Tu ji dil dixwazî vê destûrê ji bo vê malperê jê bibî? Istisnayên malperê tune - Gotarên Serekî + Gotarên serekî Tu ji dil dixwazî vê favoriyê jê bibî? @@ -1622,9 +1633,9 @@ Qada nivîsê ya ji bo navnîşana webê ya têketinê - Qada nivîsê ya ji bo navê bikarhêneriyê ya têketinê. + Qada nivîsê ya ji bo navê bikarhêneriyê yê têketinê. - Qada nivîsê ya ji bo şîfreya têketinê. + Qada nivîsê ya ji bo pêborîna têketinê. Guhertinan li hesêb tomar bike. @@ -1648,9 +1659,9 @@ Ji kerema xwe, senkronîzekirina hilpekînê veke. - Li ser amûrên din ti hilpekîneke di Firefoxê de vekirî tune ye. + Di Firefoxên li ser cîhazên te yên din de ti hilpekîneke vekirî tune ye. - Lîsteyeke hilpekînan ya ji amûrên xwe yên din bibîne. + Lîsteyeke hilpekînan a ji cîhazên xwe yên din bibîne. Têkeve Sync’ê @@ -1661,7 +1672,7 @@ Gihîşte sînorê malperên sereke - Ji bo malpereke din a sereke lê zêde bikî, yekê jê bibe. Li malperê bitikîne û li ser bihêle piştre rakirinê hilbijêre. + Ji bo malpereke din a sereke lê zêde bikî, yekê jê bibe. Li malperê bitikîne û li ser bihêle, piştre rakirinê hilbijêre. Baş e @@ -1680,7 +1691,7 @@ - Ji %sa xwe fêdeya herî zêde bibînin. + Ji %s’ê fêdeya herî zêde bibînin. Ji bo agahiyên zêdetir, bitikîne @@ -1688,7 +1699,7 @@ Tiştên ku ji te re girîng in berhev bike - Lêgerîn, malper û hilpekînên wekî hev bi hev re bike kom da ku paşê xwe zû bigihîniyê. + Lêgerîn, malper û hilpekînên wekhev, bike kom da ku paşê tu karibî xwe zû bigihînî wan. - Tu li ser vê telefonê di gerokeke din ya Firefoxê de wekî %s têketiyê. Tu dixwazî bi vî hesabî têkeviyê? + Tu li ser vê telefonê di gerokeke din a Firefoxê de wekî %s têketî yê. Tu dixwazî bi vî hesabî têkevî? diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 33f8833f0..2f9430747 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -190,6 +190,8 @@ 모양 + + 리더뷰 사용자 지정 연결할 수 없음. 인식할 수 없는 URL 구성표. @@ -227,6 +229,11 @@ 더 알아보기 + + %s 검색 + + 주소 표시줄에서 직접 검색 + 새 Firefox 탭 열기 @@ -911,16 +918,22 @@ 꺼짐 - + 오디오 및 비디오 허용 + + 오디오 및 비디오 허용 셀룰러 데이터에서만 오디오 및 비디오 차단 오디오 및 비디오가 Wi-Fi에서 재생됩니다 - + 오디오만 차단 - + + 오디오만 차단 + 오디오 및 비디오 차단 + + 오디오 및 비디오 차단 켜짐 @@ -1480,7 +1493,7 @@ 저장 안 함 - 자동완성 + 자동 채우기 Sync 로그인 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 7a58543c2..5b547d35e 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -180,6 +180,8 @@ Išvaizda + + Tinkinti skaitymo rodinį Nepavyko prisijungti. Neatpažįstama URL schema. @@ -216,6 +218,11 @@ Sužinoti daugiau + + Ieškoti „%s“ + + Ieškokite tiesiai iš adreso lauko + Atverti naują „Firefox“ kortelę @@ -607,6 +614,8 @@ Atvertos kortelės Įtraukti į rinkinį + + Pasirinkti Dalintis visomis kortelėmis @@ -887,16 +896,22 @@ Įjungta Išjungta - + Leisti garsus ir vaizdus + + Leisti garsus ir vaizdus Blokuoti garsus ir vaizdus tik naudojant mobilųjį ryšį Garsai ir vaizdai gros tik per „Wi-Fi“ - + Blokuoti tik garsus - + + Blokuoti tik garsus + Blokuoti garsus ir vaizdus + + Blokuoti garsus ir vaizdus Įjungta diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 21b4ab79d..dd3a094e1 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -183,6 +183,8 @@ Utseende + + Tilpass lesevisning Kan ikke koble til. Ugjenkjennelig URL-skjema. @@ -220,6 +222,11 @@ Les mer + + Søk %s + + Søk direkte fra adresselinjen + Åpne en ny Firefox-fane @@ -895,16 +902,22 @@ Av - + Tillat lyd og video + + Tillat lyd og video Blokker lyd og video bare på mobildata Lyd og video spilles av på Wi-Fi - + Blokker bare lyd - + + Blokker bare lyd + Blokker lyd og video + + Blokker lyd og video diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c946b5080..c9a743d91 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -187,6 +187,8 @@ Vormgeving + + Lezerweergave aanpassen Kan niet verbinden. Onherkenbaar URL-schema. @@ -224,6 +226,11 @@ Meer info + + %s doorzoeken + + Rechtstreeks vanuit de adresbalk zoeken + Een nieuw Firefox-tabblad openen @@ -894,16 +901,22 @@ Uit - + Audio en video toestaan + + Audio en video toestaan Audio en video alleen op mobiele data blokkeren Audio en video worden via wifi afgespeeld - + Alleen audio blokkeren - + + Alleen audio blokkeren + Audio en video blokkeren + + Audio en video blokkeren Aan diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml index 0c50a0747..c5f993168 100644 --- a/app/src/main/res/values-nn-rNO/strings.xml +++ b/app/src/main/res/values-nn-rNO/strings.xml @@ -183,6 +183,8 @@ Utsjånad + + Tilpass lesevising Klarte ikkje å kople til. Ukjenneleg URL-skjema. @@ -219,6 +221,11 @@ Les meir + + Søk med %s + + Søk direkte frå adresselinja + Opne ei ny Firefox-fane @@ -897,16 +904,22 @@ Av - + Tillat lyd og video + + Tillat lyd og video Blokker lyd og video berre på mobildata Lyd og video vert spela av på Wi-Fi - + Blokker berre lyd - + + Blokker berre lyd + Blokker lyd og video + + Blokker lyd og video diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index dab397e11..cbe0452dd 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -129,6 +129,8 @@ Modificar lo marcapagina Moduls + + Extensions Cap d’extension aquí @@ -532,6 +534,10 @@ Autres marcapaginas Istoric + + Onglet novèl + + Recercar dins la pagina Onglets sincronizats diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml index e65606abf..2c8bebf6a 100644 --- a/app/src/main/res/values-pa-rIN/strings.xml +++ b/app/src/main/res/values-pa-rIN/strings.xml @@ -187,6 +187,8 @@ ਦਿੱਖ + + ਪੜ੍ਹਨ ਝਲਕ ਨੂੰ ਕਸਟਮਾਈਜ਼ ਕਰੋ ਕਨੈਕਟ ਕਰਨ ਲਈ ਅਸਮਰੱਥ। ਬੇਪਛਾਣ URL ਸਕੀਮ। @@ -224,6 +226,11 @@ ਹੋਰ ਜਾਣੋ + + %s ਖੋਜ + + ਸਿਰਨਾਵਾਂ ਪੱਟੀ ਵਿੱਚੋਂ ਸਿ਼ੱਧਾ ਖੋਜੋ + ਨਵੀਂ Firefox ਟੈਬ ਖੋਲ੍ਹੋ @@ -902,16 +909,22 @@ ਬੰਦ - + ਆਡੀਓ ਅਤੇ ਵੀਡਿਓ ਦੀ ਇਜਾਜ਼ਤ ਦਿਓ + + ਆਡੀਓ ਤੇ ਵੀਡੀਓ ਦੀ ਮਨਜ਼ੂਰੀ ਦਿਓ ਸਿਰਫ਼ ਸੈਲੂਲਰ ਡਾਟਾ ਤੋਂ ਆਡੀਓ ਅਤੇ ਵੀਡੀਓ ਉੱਤੇ ਰੋਕ ਲਾਓ Wi-Fi ਉੱਤੇ ਆਡੀਓ ਤੇ ਵੀਡੀਓ ਚਲਾਏ ਜਾਣਗੇ - + ਸਿਰਫ਼ ਆਡੀਓ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਓ - + + ਸਿਰਫ਼ ਆਡੀਓ ਉੱਤੇ ਪਾਬੰਦੀ ਲਾਓ + ਆਡੀਓ ਅਤੇ ਵੀਡਿਓ ਤੇ ਪਾਬੰਦੀ ਲਾਓ + + ਆਡੀਓ ਅਤੇ ਵੀਡਿਓ \'ਤੇ ਪਾਬੰਦੀ ਲਾਓ ਚਾਲੂ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d6ffee801..5fe83ac0e 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -183,6 +183,8 @@ Wygląd + + Dostosuj widok poprawionej czytelności Nie można się połączyć. Nieznany protokół adresu URL. @@ -220,6 +222,11 @@ Więcej informacji + + Szukaj w %s + + Szukaj prosto z paska adresu + Otwórz nową kartę w Firefoksie @@ -892,16 +899,22 @@ Wyłączona - + Zezwalaj na dźwięk i wideo + + Zezwalaj na dźwięk i wideo Blokuj dźwięk i wideo tylko w sieciach komórkowych Dźwięk i wideo będą odtwarzane w sieciach Wi-Fi - + Blokuj tylko dźwięk - + + Blokuj tylko dźwięk + Blokuj dźwięk i wideo + + Blokuj dźwięk i wideo Włączone diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index eaad1028f..9c3cb4a26 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -14,7 +14,7 @@ Pesquise ou digite um endereço - Suas abas abertas serão mostradas aqui. + Suas abas abertas são mostradas aqui. Suas abas privativas são mostradas aqui. @@ -181,6 +181,8 @@ Aparência + + Personalizar leitor Não foi possível conectar. Esquema de URL não reconhecível. @@ -218,6 +220,11 @@ Saiba mais + + Pesquisar com %s + + Pesquisar diretamente na barra de endereços + Abrir uma nova aba no Firefox @@ -885,16 +892,22 @@ Ativada Desativada - + Permitir áudio e vídeo + + Permitir áudio e vídeo Bloquear áudio e vídeo apenas em rede móvel Áudio e vídeo só são reproduzidos automaticamente se o dispositivo estiver conectado a uma rede Wi-Fi - + Bloquear apenas áudio - + + Bloquear apenas áudio + Bloquear áudio e vídeo + + Bloquear áudio e vídeo Ativado @@ -1043,7 +1056,7 @@ Tem certeza que quer excluir %1$s? - Excluir esta aba também excluirá toda a coleção. Você pode criar novas coleções quando quiser. + Excluir esta aba também exclui toda a coleção. Você pode criar novas coleções quando quiser. Excluir %1$s? @@ -1189,7 +1202,7 @@ The first parameter is the name of the app (e.g. Firefox Preview) --> As configurações de privacidade e segurança bloqueiam rastreadores, código malicioso (malware) e empresas que seguem você. - Padrão (predefinido) + Normal (padrão) Bloqueia menos rastreadores. As páginas são carregadas normalmente. @@ -1289,11 +1302,11 @@ Saiba mais - Padrão (predefinido) + Normal (padrão) Bloqueia menos rastreadores. As páginas são carregadas normalmente. - O que é bloqueado pela proteção padrão contra rastreamento + O que é bloqueado pela proteção normal contra rastreamento Rigoroso diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 909deec7d..882fd490d 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -181,6 +181,8 @@ Aparência + + Personalizar a vista de leitura Não foi possível ligar. Esquema do endereço não reconhecido. @@ -218,6 +220,11 @@ Saber mais + + Pesquisar %s + + Pesquisar diretamente a partir da barra de endereço + Abrir um novo separador Firefox @@ -889,16 +896,22 @@ Ativada Desativada - + Permitir áudio e vídeo + + Permitir áudio e vídeo Bloquear áudio e vídeo apenas em dados móveis Áudio e vídeo serão reproduzidos em Wi-Fi - + Bloquear apenas o áudio - + + Bloquear apenas o áudio + Bloquear o áudio e vídeo + + Bloquear o áudio e vídeo Ativado diff --git a/app/src/main/res/values-rm/strings.xml b/app/src/main/res/values-rm/strings.xml index 7736f8f3d..c996dbdcb 100644 --- a/app/src/main/res/values-rm/strings.xml +++ b/app/src/main/res/values-rm/strings.xml @@ -178,6 +178,8 @@ Apparientscha + + Persunalisar la vista da lectura Impussibel da connectar. Schema dad URL nunenconuschent. @@ -214,6 +216,11 @@ Ulteriuras infurmaziuns + + Tschertgar cun %s + + Tschertgar direct en la trav d\'adressas + Avrir in nov tab da Firefox @@ -879,16 +886,22 @@ Activà Deactivà - + Permetter audio e video + + Permetter audio e video Bloccar audio e video mo en la rait mobila Audio e video vegn reproducì en cas dad ina connexiun WLAN - + Bloccar mo l\'audio - + + Bloccar mo l\'audio + Bloccar audio e video + + Bloccar audio e video Activà diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 758628cfa..be9d8929f 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -192,6 +192,8 @@ Оформление + + Настроить вид для чтения Ошибка подключения. Не удалось распознать схему URL. @@ -229,6 +231,11 @@ Узнать больше + + Поиск в %s + + Поиск прямо из адресной строки + Открыть новую вкладку Firefox @@ -913,16 +920,22 @@ Отключена - + Разрешить аудио и видео + + Разрешить аудио и видео Блокировать аудио и видео только при использовании мобильных данных Аудио и видео будут проигрываться при использовании Wi-Fi - + Блокировать только аудио - + + Блокировать только аудио + Блокировать аудио и видео + + Блокировать аудио и видео Включено @@ -1232,7 +1245,7 @@ Автоматическая приватность - Настройки конфиденциальности и безопасности блокируют трекеры, вредоносные программы, и компании, которые вас отслеживают. + Настройки приватности и безопасности блокируют трекеры, вредоносные программы, и компании, которые вас отслеживают. Стандартная (по умолчанию) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 08904d2f5..5a6d40324 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -220,6 +220,11 @@ Ďalšie informácie + + Hľadať cez %s + + Vyhľadať na webe priamo z panela s adresou + Otvoriť novú kartu vo Firefoxe @@ -894,16 +899,22 @@ Vypnutá - + Povoliť zvuk a video + + Povoliť zvuk a video Blokovať automatické prehrávanie zvuku a videí pri pripojení cez mobilné dáta Zvuk a video sa bude automaticky prehrávať len na Wi-Fi - + Blokovať len zvuk - + + Blokovať len zvuk + Blokovať zvuk a video + + Blokovať zvuk a video Zapnuté diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b722f3d88..7bcfaa4dc 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -181,6 +181,8 @@ Videz + + Prilagodi bralni pogled Povezava ni mogoča. Neprepoznavna URL shema. @@ -217,6 +219,11 @@ Več o tem + + Išči z %s + + Iščite neposredno iz naslovne vrstice + Odpri nov zavihek v Firefoxu @@ -896,17 +903,23 @@ Vklopljeno Izklopljeno - + Dovoli zvok in video + + Dovoli zvok in video Zavrni zvok in video samo na mobilnih podatkih Zvok in Video se bosta predvajala na Wi-Fi - + Zavrni samo zvok - + + Zavrni samo zvok + Zavrni zvok in video + + Zavrni zvok in video Vključeno diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index b5c2d8f05..8df0287d8 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -128,6 +128,8 @@ Përpunoni faqerojtësin Shtesa + + Zgjerime S’ka shtesa këtu @@ -525,6 +527,10 @@ Faqerojtës të Tjerë Historik + + Skedë e re + + Gjej në faqe Skeda të njëkohësuara diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index bec6b2f74..e4a6bc50d 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -215,6 +215,11 @@ Сазнајте више + + Претражи %s + + Претражи директно из адресне траке + Отворите нови Firefox језичак @@ -886,16 +891,22 @@ Укључено Искључено - + Дозволи звук и видео + + Дозволи звук и видео Блокирај звук и видео само на мобилној мрежи Звук и видео биће пуштани на бежичној мрежи - + Блокирај само звук - + + Блокирај само звук + Блокирај звук и видео + + Блокирај звук и видео Укључено diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 7ce8f19e5..4dc5c7319 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -128,6 +128,8 @@ Émboh + + Éksténsi Teu aya add-on di dieu @@ -214,6 +216,11 @@ Lenyepan + + Paluruh %s + + Paluruh langsung tina bilah alamat + Buka tab Firefox anyar @@ -533,6 +540,10 @@ Markah Lianna Jujutan + + Tab anyar + + Téangan dina kaca Tab singkron @@ -887,16 +898,22 @@ Hurung Pareum - + Ngidinan sora jeung pidéo + + Idinan audio jeung pidéo Blokir sora jeung pidéo dina data sélulér wungkul Sora jeung pidéo bakal dimaénkeun dina Wi-Fi - + Blokir sora hungkul - + + Peungpeuk audio hungkul + Blokir sora jueng pidéo + + Peungpeuk audio jeung pidéo Hurung diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 089dd5782..6edd64c30 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -185,6 +185,8 @@ Utseende + + Anpassa läsvyn Kan inte ansluta. Oigenkännligt URL-schema. @@ -222,6 +224,11 @@ Läs mer + + Sök med %s + + Sök direkt från adressfältet + Öppna en ny Firefox-flik @@ -897,16 +904,22 @@ Av - + Tillåt ljud och video + + Tillåt ljud och video Blockera ljud och video endast vid mobildata Ljud och video spelas upp på Wi-Fi - + Blockera ljud endast - + + Blockera endast ljud + Blockera ljud och video + + Blockera ljud och video diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml index f380493ee..af1b29dbe 100644 --- a/app/src/main/res/values-tg/strings.xml +++ b/app/src/main/res/values-tg/strings.xml @@ -130,6 +130,8 @@ Таҳрир кардани хатбарак Ҷузъҳои иловагӣ + + Васеъшавиҳо Дар ин ҷо ягон ҷузъи иловагӣ нест @@ -179,6 +181,8 @@ Намуди зоҳирӣ + + Фармоишдиҳии намоиши хониш Пайваст нашуд. Нақшаи URL шинохтанашаванда аст. @@ -215,6 +219,11 @@ Маълумоти бештар + + Ҷустуҷӯ дар %s + + Ҷустуҷӯи бевосита аз навори нишонӣ + Кушодани варақаи нави Firefox @@ -525,6 +534,10 @@ Хатбаракҳои дигар Таърих + + Варақаи нав + + Ҷустуҷӯ дар саҳифа Варақаҳои ҳамоҳангшуда @@ -880,16 +893,22 @@ Ғайрифаъол - + Иҷозат додани аудио ва видео + + Иҷозат додани аудио ва видео Бастани аудио ва видео танҳо дар шабакаи маълумоти мобилӣ Аудио ва видео дар шабакаи Wi-Fi пахш мешаванд - + Бастани танҳои аудио - + + Бастани танҳои аудио + Бастани аудио ва видео + + Бастани аудио ва видео Фаъол diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 434e4e567..fd57935fc 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -129,6 +129,8 @@ แก้ไขที่คั่นหน้า ส่วนเสริม + + ส่วนขยาย ไม่มีส่วนเสริมที่นี่ @@ -214,6 +216,11 @@ เรียนรู้เพิ่มเติม + + ค้นหา %s + + ค้นหาโดยตรงจากแถบที่อยู่ + เปิดแท็บ Firefox ใหม่ @@ -526,6 +533,10 @@ ที่คั่นหน้าอื่น ๆ ประวัติ + + แท็บใหม่ + + ค้นหาในหน้า แท็บที่ซิงค์ @@ -878,16 +889,22 @@ เปิด ปิด - + อนุญาตเสียงและวิดีโอ + + อนุญาตเสียงและวิดีโอ ปิดกั้นเสียงและวิดีโอบนการใช้งานมือถือ เสียงและวิดีโอจะเล่นบน Wi-Fi - + ปิดกั้นเสียงเท่านั้น - + + ปิดกั้นเสียงเท่านั้น + ปิดกั้นเสียงและวิดีโอ + + ปิดกั้นเสียงและวิดีโอ เปิด diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml index 93f5ed5e4..17ede797b 100644 --- a/app/src/main/res/values-tl/strings.xml +++ b/app/src/main/res/values-tl/strings.xml @@ -50,14 +50,24 @@ Napili - %1$s ay gawa ng Mozilla. + Ang %1$s ay gawa ng Mozilla. Ikaw ay nasa pribadong sesyon + + + Inaalis ng %1$s ang iyong bakas sa paghahanap at pag-browse sa mga pribadong tab kapag isinara mo sila o umalis sa app. Bagaman makikilala ka pa rin ng mga website o internet service provider mo, nakatutulong pa rin ito para mapanatiling pribado ang mga gawain mo online mula sa ibang gagamit ng device na ito. + + Mga karaniwang pamahiin tungkol sa pribadong pag-browse + Burahin ang sesyon + + + Magdagdag ng shortcut para makapagbukas ng mga pribadong tab mula sa iyong Home screen. Magdagdag ng shortcut @@ -78,11 +88,15 @@ Alisin + + Itakda na kusang magsara ang mga nakabukas na tab na hindi napansin sa nakaraang araw, linggo, o buwan. Tingnan ang mga pagpipilian Alisin + + Baguhin ang layout ng mga nakabukas na tab. Pumunta sa mga setting at piliin ang grid sa tab view. Pumunta sa settings @@ -113,6 +127,8 @@ i-Edit ang Bookmark Mga Add-on + + Mga extension Walang mga add-on dito @@ -149,10 +165,10 @@ Buksan sa %1$s - POWERED BY %1$s + PINATATAKBO NG %1$s - Powered by %1$s + Pinatatakbo ng %1$s Reader view @@ -175,6 +191,9 @@ Sundin ang wika sa device + + Wika ng paghanap + i-Scan @@ -193,9 +212,16 @@ Payagan ang mga mungkahi sa paghahanap sa mga pribadong session? + + Ibabahagi ng %s ang lahat ng ita-type mo sa address bar sa iyong default na search engine. Alamin + + Hanapin sa %s + + Maghanap direkta mula sa address bar + Magbukas ng bagong Firefox tab @@ -216,8 +242,10 @@ Pangkalahatan Tungkol sa + + Default na search engine - Hanapin + Paghahanap Address bar @@ -230,13 +258,15 @@ The first parameter is the name of the app defined in app_name (for example: Fenix) --> Tungkol sa %1$s - Your Rights + Ang Iyong mga Karapatan Mga Password Mga credit card at tirahan Itakda bilang default browser + + Advanced Pribasiya @@ -249,10 +279,18 @@ Buksan ang mga link sa pribadong tab Payagan ang mga screenshot sa private browsing + + Kapag pinayagan, makikita rin ang mga pribadong tab kapag maraming nakabukas na app Magdagdag ng private browsing shortcut Accessibility + + Pasadya na Firefox Account server + + Pasadya na Sync server + + Nabago ang Firefox Account/Sync server. Magsasara na ang application para mailapat ang mga pagbabago… Account @@ -264,9 +302,9 @@ Home - Gestures + Mga gesture - Customize + Ipasadya Mag-sync ng mga bookmark, kasaysayan, at iba pa gamit ang iyong Firefox Account @@ -281,11 +319,13 @@ Mga pamimilian sa data Pagkolekta ng data + + Paunawa sa privacy Developer tools - Remote debugging via USB + Remote debugging sa USB Ipakita ang mga search engine @@ -311,11 +351,14 @@ Hiwalay na download manager - Add-ons + Mga add-on Mga abiso + + + Pasadyang koleksyon ng mga Add-on OK @@ -327,6 +370,9 @@ May-ari ng collection (User ID) + + Nabago ang Add-on collection. Magsasara na ang application para mailapat ang mga pagbabago… + Hindi suportado ang add-on @@ -335,7 +381,7 @@ - Sync now + Mag-sync na Piliin alin ang isi-sync @@ -345,16 +391,16 @@ Mga Login - Open tabs + Mga nakabukas na tab - mag-Sign out + Mag-sign out Ngalan ng device Hindi maaaring blangko ang pangalan ng device. - Syncing… + Nagsi-Sync… Bigong pag-sync. Huling matagumpay na pag-sync: %s @@ -366,11 +412,13 @@ - %1$s ng %2$s %3$s + %1$s sa %2$s %3$s Mga natanggap na tab + + Mga abiso para sa mga tab na natanggap mula sa ibang mga Firefox device. May natanggap na tab @@ -396,17 +444,28 @@ Alamin + + Nakasara sa pangkalahatan, pumunta sa Mga Setting para buksan ito. + Telemetry Usage at technical data + + Ibinabahagi sa Mozilla ang datos patungkol sa performance, paggamit, hardware at customization sa browser mo para mapaganda lalo ang %1$s Marketing data + + Ibinabahagi ang datos tungkol sa mga feature na ginagamit mo sa %1$s kay Leanplum, ang aming mobile marketing vendor. Mga pag-aaral + + Pinapayagan ang Mozilla para magkabit at magpatakbo ng mga pag-aaral Mga eksperimento + + Pinapayagan ang Mozilla para magkabit at magkolekta ng datos para sa mga eksperimental na feature Crash reporter @@ -417,19 +476,19 @@ - Turn on Sync + Buksan ang Sync Scan pairing code in desktop Firefox - Sign in + Mag-sign in - Sign in to reconnect + Mag-sign in para mag-reconnect - Remove account + Tanggalin ang account - firefox.com/pair]]> + firefox.com/pair]]> Buksan ang Camera @@ -443,10 +502,12 @@ - Light + Maliwanag Madilim + + Nakatakda sa Battery Saver Sundin ang tema ng device @@ -479,6 +540,10 @@ Iba pang mga Bookmark Kasaysayan + + Bagong tab + + Hanapin sa pahina Mga naka-sync na tab @@ -487,13 +552,15 @@ Hanapin Mga Setting + + Menu para sa listahan ng kasaysayan Isara Mga isinarang tab kamakailan - Ipakita ang kumpletong kasaysayan + Ipakita ang buong kasaysayan %d mga tab @@ -507,6 +574,8 @@ Mga Tab + + Tab view Listahan @@ -562,12 +631,20 @@ Isara lahat ng mga tab Bagong Tab + + Pumunta sa home + + i-Toggle ang tab mode Bookmark Isara Ibahagi ang mga piniling mga tab + + Menu para sa mga napiling tab + + Tanggalin ang tab sa collection Piliin ang mga tab @@ -643,7 +720,7 @@ Burahin - Napili na ang %1$d + May %1$d napili Burahin ang %1$d item @@ -676,7 +753,7 @@ Walang mga naka-download na file - Napili na ang %1$d + May %1$d napili Buksan @@ -703,6 +780,11 @@ Ibahagi ang session + + + Bookmark menu + + i-Edit ang bookmark Pumili ng folder @@ -738,7 +820,7 @@ i-Save - Napili na ang %1$d + May %1$d napili i-Edit ang bookmark @@ -777,45 +859,874 @@ Mga permiso Pumunta sa mga Setting + + Listahan ng mga quick setting + + Inirerekomenda + + i-Manage ang mga site permission + + Alisin ang pahintulot + + Alisin ang pahintulot + + Alisin ang mga pahintulot sa lahat ng site + + Autoplay Camera Mikropono Lokasyon + + Mga abiso + + Persistent Storage + + DRM-controlled content + + Tanungin para payagan + + Hinarang + + Pinapayagan + + Hinarang ng Android + + Mga exception + + Nakabukas + + Nakasara + + Payagan ang audio at video + + Payagan ang audio at video + + Harangin ang audio at video kapag naka-cellular data lamang + + Aandar ang audio at video kapag naka-Wi-Fi + + Harangin ang audio lamang + + Harangin ang audio lamang + + Harangin ang audio at video + + Harangin ang audio at video - On + Nakabukas - Off + Nakasara Mga Koleksyon + + Collection menu + + Kolektahin ang mga bagay na mahalaga sa iyo.\nPangkatin ang mga magkakatulad na hinahanap, site, at tab para madaling makuha sa susunod. + + Piliin ang mga Tab + + Piliin ang collection + + Pangalanan ang collection + + Magdagdag ng bagong collection Piliin lahat + + Tanggalin lahat sa pagkakapili + + Piliin ang mga tab na ise-save + + %d piniling mga tab + + %d piniling tab + + Na-save na ang mga tab! + + Na-save na ang collection! + + Na-save na ang tab! Isara + + i-Save + + Tingnan + + + Ika-%d collection + + + + Ipadala at Ibahagi Ibahagi Ibahagi + + Magbahagi ng link + + Ipadala sa device + + Lahat ng pagkilos + + Kagagamit lamang + + Mag-sign in sa Sync + + Ipadala sa lahat ng mga device + + Kumonekta muli sa Sync + + Offline + + Magkonekta ng isa pang device + + Para makapagpadala ng tab, mag-sign in sa Firefox sa isa pang device. + + Nakuha ko + + Hindi kayang maibahagi sa app na ito + + Ipadala sa device + + Walang Nakakonektang mga Device + + Alamin ang Tungkol sa Pagpapadala ng mga Tab… + + Magkonekta ng Isa Pang Device… + + + + Private browsing session + + Burahin ang mga pribadong tab + + Isara ang mga pribadong tab + + Buksan + + Burahin at Buksan + + Pinatatakbo Ng + + Nabura na ang collection + + Nabago na ang pangalan ng collection + + Nabura na ang tab + + Nabura na ang mga tab + + Naisara na ang tab + + Naisara na ang mga tab + + Naisara na ang mga tab! + + Na-save na ang mga bookmark! Tingnan + + Naidagdag na sa mga top site! + + Naisara na ang pribadong tab + + Naisara na ang mga pribadong tab + + Nabura na ang mga pribadong tab + + i-UNDO + + Natanggal na ang site + + i-Undo + + Kumpirmahin + + Payagan ang %1$s na buksan ang %2$s + + PAYAGAN + + TANGGIHAN + + Sigurado ka bang gusto mong burahin ang %1$s? + + Ang pagbura sa tab na ito ay magdudulot ng pagkabura ng buong collection. Maaari kang gumawa ng bagong collection anumang oras. + + Burahin ang %1$s? + + Burahin + + Kanselahin + + Pumapasok sa full screen mode Kopyahin ang URL + + Ito ay halimbawa lamang. Nandito ang tekstong ito para maipakita ang itsura kapag nilakihan o niliitan ang sukat sa setting na ito. + + Palakihin o paliitin ang teksto sa mga website + + Laki ng Font + + + Awtomatikong sukat ng font + + Ang laki ng font ay itatapat sa setting sa iyong Android. I-disable para ma-manage ang laki ng font dito. + + + Burahin ang browsing data + + Mga nakabukas na tab + + %d tab + + Kasaysayan ng pag-browse at site data + + %d address + + Kasaysayan + + %d pahina + + Mga cookie + + Mala-log out ka sa karamihan ng mga site + + Mga naka-cache na larawan at file + + Nagpapaluwag ng storage space + + Mga site permission + + Mga download + + Burahin ang browsing data + + Burahin ang browsing data sa pag-alis + + Kusang magbubura ng browsing data kapag pinili mo ang "Umalis" mula sa main menu + + + Kusang magbubura ng browsing data kapag pinili mo ang \"Umalis\" mula sa main menu + + Umalis + + + Buburahin nito lahat ng iyong browsing data. + + Buburahin ng %s ang mga napiling browsing data. + + Kanselahin + + Burahin + + Nabura na ang browsing data + + Binubura ang browsing data… + + + + Ang Firefox Preview ay Firefox Nightly na ngayon + + + Gabi-gabing naa-update ang Firefox Nightly at may mga eksperimental na bagong feature. + Subalit maaari itong maging sirain. I-download ang aming beta browser para sa mas panatag na karanasan. + + Kunin ang Firefox para sa Android Beta + + + Lumipat na ang Firefox Nightly + + + Ang app na ito ay hindi na makatatanggap ng mga security update. Tigilan nang gamitin ang app na ito at lumipat na sa bagong Nightly. + \n\nPara mailipat ang iyong mga bookmark, login, at kasaysayan sa isa pang app, gumawa ng isang Firefox account. + + Lumipat na sa bagong Nightly + + + Lumipat na ang Firefox Nightly + + + Ang app na ito ay hindi na makatatanggap ng mga security update. Kunin na ang bagong Nightly at tigilan nang gamitin ang app na ito. + \n\nPara mailipat ang iyong mga bookmark, login, at kasaysayan sa isa pang app, gumawa ng isang Firefox account. + + Kunin na ang bagong Nightly + + + + Maligayang pagdating sa %s! + + Mayroon nang account? + + Kilalanin ang %s + + Tingnan kung ano ang bago + + May mga katanungan tungkol sa bagong-disenyong %s? Nais malaman alin ang nabago? + + Kumuha ng mga kasagutan dito + + Magsimulang mag-sync ng mga bookmark, password, at iba pa gamit ang iyong Firefox account. + + + Alamin + + Naka-sign in ka bilang %s sa isa pang Firefox browser sa device na ito. Nais mo bang mag-sign in gamit ito? + + Oo, i-sign in mo ako + + Nagsa-sign in… + + Mag-sign in sa Firefox + + Manatiling naka-sign out + + Nakabukas ang Sync + + Bigong makapag-sign-in + + Awtomatikong pribasiya + + Hinaharang ng mga setting ng pribasiya at seguridad ang mga tracker, malware, at kumpanyang nagmamanman sa iyo. + + Karaniwan (default) + + Humaharang ng mas kaunting mga tracker. Normal na maglo-load ang mga pahina. + + Mahigpit (inirerekomenda) + + Mahigpit + + Humaharang ng mas maraming mga tracker, ad, at popup. Mas mabilis maglo-load ang mga pahina, pero baka may mga bagay na hindi gagana. + + Makibaka + + Subukan ang pag-browse sa isang kamay gamit ang toolbar sa ibaba o ilipat ito sa tuktok. + + Mag-browse nang pribado + + Magbukas ng isang pribadong tab: I-tap ang %s icon. + + Magbukas ng pribadong tab sa bawat pagkakataon: I-update ang iyong mga setting sa pribadong pag-browse. + + Buksan ang mga setting + + Ang iyong pribasiya + + + Dinisenyo namin ang %s para bigyan ka ng kontrol sa mga ibinahabagi mo online at kung ano ang ibinabahagi mo sa amin. + + Basahin ang aming paunawa sa privacy + + Isara + + + Simulan nang mag-browse + + + + Pumili ng iyong tema + + Makatipid sa baterya at iyong paningin gamit ang dark mode. + + Kusa + + Umaangkop sa mga setting ng iyong device + + Madilim na tema + + Maliwanag na tema + + + Napadala na ang mga tab! + + Napadala na ang tab! + + Bigong mapadala + + SUBUKAN MULI + + i-Scan ang code + + https://firefox.com/pair]]> + + Handa nang mag-scan + + Mag-sign in gamit ang iyong camera + + Gumamit na lang ng email + + Gumawa ng isa para makapag-sync ng Firefox sa mga device.]]> + + Titigil na ang Firefox sa pag-sync sa iyong account, pero hindi nito buburahin ang kahit anong browsing data sa device na ito. + + Titigil na ang %s sa pag-sync sa iyong account, pero hindi nito buburahin ang kahit anong browsing data sa device na ito. + + Mag-disconnect + + Kanselahin + + Hindi ma-edit ang mga default folder + + + + Mga Protection Setting + + Enhanced Tracking Protection + + Mag-browse nang hindi sinusundan + + Itago mo ang data na para sa iyo lamang. Pinoprotektahan ka ng %s mula sa mga pinakakaraniwang tracker na nagmamanman sa mga gawain mo online. + + Alamin + + Karaniwan (default) + + Humaharang ng mas kaunting mga tracker. Normal na maglo-load ang mga pahina. + + Ano ang mga hinaharang sa karaniwang tracking protection + + Mahigpit + + Humaharang ng mas maraming mga tracker, ad, at popup. Mas mabilis maglo-load ang mga pahina, pero baka may mga bagay na hindi gagana. + + Ano ang mga hinaharang sa mahigpit na tracking protection + + Pasadya + + + Piliin kung aling mga tracker at script ang dapat harangin. + + Ano ang mga hinaharang sa pasadyang tracking protection + + + Mga cookie + + Mga cross-site at social media tracker + + Mga cookie mula sa mga hindi pa nabisitang site + + Lahat ng mga third-party na cookie (maaaring maging sanhi upang masira ang mga website) + + Lahat ng mga cookie (tiyak na ikasisira ng mga website) + + Tracking content + + Sa lahat ng mga tab + + Sa mga Pribadong tab lamang + + Sa mga Pasadyang tab lamang + + Mga cryptominer + + Mga fingerprinter + Hinarang + + Pinapayagan + + Mga Social Media Tracker + + Nililimitahan ang kakayahan ng mga social network para subaybayan ang iyong browsing activity sa web. + + Mga Cross-Site Tracking Cookie + + Hinaharang ang mga cookie na ginagamit ng mga ad network at analytics company para pagsama-samahin ang iyong browsing data sa iba\'t-ibang mga site. + + Mga cryptominer + + Pinipigilan ang mga mapanghamak na script sa pag-access sa device mo para magmina ng digital currency. + + Mga fingerprinter + + Pinipigilang makolekta ang mga bukod-tanging data ng pagkakakilanlan sa device mo na pwedeng gamitin sa pagsubaybay. + + Tracking Content + + Pinipigilang mag-load ang mga panlabas na mga ad, video, at iba pang content na nagtataglay ng tracking code. Maaaring maapektuhan ang ilang aspeto ng website. + + Tuwing kulay lila ang kalasag, may naharang na mga tracker ang %s sa site. I-tap para sa karagdagang impormasyon. + + NAKABUKAS ang mga protection sa site na ito + + SARADO ang mga protection sa site na ito + + Sarado ang Enhanced Tracking Protection sa mga website na ito + + Bumalik + + Ang iyong mga karapatan + + Mga open source library na ginagamit namin + + Ano’ng bago sa %s + + %s | Mga OSS Library + + Mga Redirect Tracker + + + Inaalis ang mga cookie na itinakda ng mga redirect sa mga kilalang tracking website. + + + Suporta + + Mga crash + + Paunawa sa privacy + + Alamin ang iyong mga karapatan + + Impormasyon sa paglisensiya + + Mga library na ginagamit namin + + + Debug menu: %1$d natitirang click para ma-enable + Naka-enable na ang debug menu + + + 1 tab + + %d tab + Kopyahin + + i-Paste at Puntahan + + i-Paste + + Nakopya na ang URL sa clipboard + + + Idagdag sa Home screen + + + Kanselahin + + Idagdag + + Magpatuloy sa website + + Pangalan ng shortcut + + Madali lang maidagdag ang website na ito sa Home screen ng iyong device para agarang ma-access at ma-browse na parang app lamang. + + + Mga login at password + + i-Save ang mga login at password + + Tanungin kung ise-save + + Huwag mag-save kailanman + + Autofill + + Mag-sync ng mga login + + Nakabukas + + Nakasara + + Kumonekta muli + + Mag-sign in sa Sync + + Mga naka-save na login + + Ang mga login na naka-save o naka-sync sa %s ay magpapakita rito. + + Alamin ang tungkol sa Sync. + + Mag exception + + Ang mga login at password na hindi naka-save ay ipapakita rito. + + Hindi ise-save ang mga login at password para sa mga site na ito. + + Burahin lahat ng mga exception + + Hanapin sa mga login + + Alpabetikal + + Ginamit kamakailan + + Site + + Username + + Password + + Muling ipasok ang PIN + + i-Unlock para makita ang mga naka-save na login + + Ang koneksyong ito ay di-ligtas. Maaaring makompromiso ang mga login na ipapasok dito. + + Alamin + + Gusto mo bang i-save ng %s ang login na ito? + + i-Save + + Huwag i-save + + Nakopya na ang password sa clipboard + + Nakopya na ang username sa clipboard + + Nakopya na ang site sa clipboard Kopyahin ang password + + Alisin ang password Kopyahin ang username + + Alisin ang username Kopyahin ang site - + + Buksan ang site sa browser + + Ipakita ang password + + Itago ang password + + i-Unlock para makita ang mga naka-save na login + + Pakaingatan ang iyong mga login at password + + + Magtakda ng device lock pattern, PIN, o password para maprotektahan ang mga naka-save mong login at password sakaling may ibang may hawak sa device mo. + + Mamaya + + Mag-set up ngayon din + + i-Unlock ang iyong device + + Mag-zoom sa lahat ng mga website + + Paganahin para makapag-pinch at zoom, kahit sa mga website na pinipigilan ang gesture na ito. + + Pangalan (A-Z) + + Huling ginamit + + + Menu sa pagsunud-sunod ng login + + + Magdagdag ng search engine + + i-Edit ang search engine + + Magdagdag + + i-Save + + i-Edit + + Burahin + + + Iba pa + + Pangalan + + + Gagamiting teksto ng paghahanap + + Palitan ang query ng “%s”. Halimbawa:\nhttps://www.google.com/search?q=%s + + Alamin + + Pasadyang detalye ng search engine + + + Link para sa Alamin + + + Ipasok ang pangalan ng search engine + + Mayroon nang search engine na nagngangalang “%s”. + + Magpasok ng search string + + Siguruhing tugma ang search string sa format ng halimbawa + + Nagkaproblema kumonekta sa “%s” + + Nalikha na ang %s + + Nai-save na ang %s + + Nabura na ang %s + + + Maligayang pagdating sa isang bagung-bagong %s + + Nasa harapan mo ang isang bagong-disenyong browser, na pinaghusay ang performance at mga feature na makatutulong sa iyo nang husto online.\n\nMangyaring maghintay habang ina-update namin ang %s kasama ang iyong + + Ina-update ang %s… + + Simulan ang %s + + Tapos na ang pag-angkat + + Mga password + + + Para payagan ito: + + 1. Pumunta sa Android Settings + + Mga pahintulot]]> + + %1$s sa nakabukas]]> + + + Ligtas na Koneksyon + + Hindi Ligtas na Koneksyon + + Sigurado ka bang gusto mong alisin lahat ng mga pahintulot sa lahat ng mga site? + + Sigurado ka bang gusto mong alisin lahat ng mga pahintulot sa site na ito? + + Sigurado ka bang gusto mong alisin itong pahintulot sa site na ito? + + Walang mga site exception + + Mga Pangunahing Artikulo + + Sigurado ka bang gusto mong burahin itong bookmark? + + Idagdag sa mga top site + + + Pinatunayan Ni: %1$s + + Burahin + + i-Edit + + Sigurado ka bang gusto mong burahin itong login? + + Burahin + + Mga pagpipilian sa login + + Ang editable text field para sa web address ng login. + + Ang editable text field para sa username ng login. + + Ang editable text field para sa password ng login. + + i-Save ang mga pagbabago sa login. + + Isantabi ang mga pagbabago + + i-Edit + + Kinakailangan ang password + + Paghanap gamit ang boses + + Magsalita na + + Mayroon nang login na may ganyang username + + + + Magkonekta ng isa pang device. + + Mangyaring mag-re-authenticate. + + Paki-enable ang pag-sync ng tab. + + Wala kang mga nakabukas na tab sa Firefox sa iba mo pang mga device. + + Tingnan ang listahan ng mga tab mula sa iba mong mga device. + + Mag-sign in sa sync + + Walang nakabukas na mga tab + + + + Naabot na ang hangganan ng bilang sa top site + + Magtanggal ng isa para makapagdagdag ng bagong pangunahing site. Pindutin nang matagal ang site at piliin ang tanggalin. + + OK, Nakuha ko + + Ipakita ang pinakamadalas na bisitahing site + + + Pangalan + + Pangalan ng pangunahing site + + OK + + Kanselahin + + + Tanggalin + + + Sulitin ang %s. + + + Pindutin para sa karagdagang detalye + + + Kolektahin ang mga bagay na mahalaga sa iyo + + Pangkatin ang mga magkakatulad na hinahanap, site, at tab para madaling makuha sa susunod. + + Naka-sign in ka bilang %s sa isa pang Firefox browser sa teleponong ito. Nais mo bang mag-sign in gamit ito? + + Madali lang maidagdag ang website na ito sa Home screen ng iyong telepono para agarang ma-access at ma-browse na parang app lamang. + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index f6016615c..cc9cdc8df 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -181,6 +181,8 @@ Görünüm + + Okuyucu görünümünü özelleştir Bağlanılamıyor. Tanınmayan URL şeması. @@ -218,6 +220,11 @@ Daha fazla bilgi al + + %s ile ara + + Doğrudan adres çubuğundan arama yapın + Yeni Firefox sekmesi aç @@ -887,16 +894,22 @@ Kapalı - + Ses ve videoya izin ver + + Ses ve videoya izin ver Ses ve videoyu yalnızca hücresel veride engelle Ses ve videolar yalnızca Wi-Fi’da oynatılacak - + Yalnızca sesi engelle - + + Yalnızca sesi engelle + Ses ve videoyu engelle + + Ses ve videoyu engelle Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index aca454f05..984dc782d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -69,7 +69,7 @@ - Додайте ярлик для відкриття приватних вкладок з головного екрану. + Додайте ярлик для відкриття приватних вкладок з головного екрана. Додати ярлик @@ -182,6 +182,8 @@ Вигляд + + Налаштувати режим читача Неможливо з’єднатися. Невідома схема URL-адреси. @@ -219,6 +221,11 @@ Докладніше + + Пошук в %s + + Пошук безпосередньо з панелі адреси + Відкрити нову вкладку Firefox @@ -522,7 +529,7 @@ Сеанси - Знімки екрану + Знімки екрана Завантаження @@ -893,16 +900,22 @@ Увімкнено Вимкнено - + Дозволити аудіо та відео + + Дозволити аудіо та відео Блокувати аудіо та відео лише для стільникових даних Аудіо та відео відтворюватимуться через Wi-Fi - + Блокувати лише аудіо - + + Блокувати лише аудіо + Блокувати аудіо та відео + + Блокувати аудіо та відео Увімкнено @@ -1079,7 +1092,7 @@ Видалити дані перегляду - Відкрити вкладки + Відкриті вкладки Вкладок: %d diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 45ee1537f..d43fd38a6 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -77,6 +77,8 @@ برخاست کریں + + کیمرے تک رسائی کی ضرورت ہے۔ Android کی ترتیبات پر جائیں ، اجازتیں ٹیپ کریں ، اور اجازت پر ٹیپ کریں۔ سیٹنگز پر جائیں @@ -118,6 +120,8 @@ بُک مارک کی تدوین کریں ایڈ اون + + ایکسٹینشنز یہاں کوئی ایڈونس نہیں ہے @@ -205,6 +209,9 @@ مزید سیکھیں + + %s تلاش کریں + ایک نیا Firefox ٹیب کھولیں @@ -262,6 +269,8 @@ لنکس نجی ٹیب میں کھولیں نجی براؤزنگ میں اسکرین شاٹس کی اجازت دیں + + اگر اجازت دی جائے تو ، جب ایک سے زیادہ اطلاقات کھلے ہیں تو نجی ٹیبز بھی نظر آئیں گے نجی براؤزنگ شارٹکٹ شامل کریں @@ -345,6 +354,12 @@ مجموعہ کرنے والا مالک (صارف شناخت) + + + ایڈ آن کی معاونت نہیں ہے + + ایڈ آن پہلے ہی نصب ہے + ابھی sync کریں @@ -478,6 +493,11 @@ آلہ کے تھیم پر عمل کریں + + + تازہ کرنے کے لیے کھینچیں + + ٹول بار کو چھپانے کے لئے اسکرول کریں ٹیبز کو کھولنے کے لئے ٹول بار کو سوائپ کریں @@ -500,6 +520,10 @@ دیگر نشانیاں سابقات + + نیا ٹیب + + صفحے میں تلاش کریں سینک ہو چکے ٹیبز @@ -530,6 +554,8 @@ ٹیبس + + ٹیب ویو فہرست @@ -595,6 +621,8 @@ بند كریں منتخب شدہ ٹیبز کا اشتراک کریں + + منتخب کردہ ٹیبز مینو مجموعہ سے ٹیب کو ہٹا دیں @@ -693,10 +721,14 @@ ڈاؤن لوڈ حذف کریں + + کیا آپ واقعی اپنے ڈاؤن لوڈ صاف کرنا چاہتے ہیں؟ ڈاؤن لوڈز کو ہٹا دیا گیا %1$s ہٹائیں + + ڈاؤن لوڈ فائلیں نہیں ہیں %1$d منتخب کیا گیا @@ -829,6 +861,8 @@ مقام اعلانات + + مستقل اسٹوریج DRM کے زیرانتظام مواد @@ -846,16 +880,22 @@ چالو بند - + آڈیو اور ویڈیو کی اجازت دیں + + آڈیو اور ویڈیو کی اجازت دیں صرف سیلولر ڈیٹا پر آڈیو اور ویڈیو بلاک کریں Wi-Fi پر آڈیو اور ویڈیو چلے گی - + صرف آڈیو بلاک کریں - + + صرف آڈیو کو مسدود کریں + آڈیو اور ویڈیو بلاک کریں + + آڈیو اور ویڈیو کو مسدود کریں چالو @@ -1050,6 +1090,8 @@ ذخیرہ کار جگہ کو خالی کرتا ہے سائٹ کی اجازت + + ڈاؤن لوڈز براؤزنگ کا ڈیٹا مٹائیں diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index c7ac27a9a..8d14f5ecf 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -181,6 +181,8 @@ Giao diện + + Tùy chỉnh chế độ đọc Không thể kết nối. Không thể nhận dạng URL. @@ -217,6 +219,11 @@ Tìm hiểu thêm + + Tìm kiếm %s + + Tìm kiếm trực tiếp từ thanh địa chỉ + Mở một thẻ Firefox mới @@ -883,16 +890,22 @@ Bật Tắt - + Cho phép âm thanh và video + + Cho phép âm thanh và video Chỉ chặn âm thanh và video trên dữ liệu di động Âm thanh và video sẽ phát trên Wi-Fi - + Chỉ chặn âm thanh - + + Chỉ chặn âm thanh + Chặn âm thanh và video + + Chặn âm thanh và video Bật diff --git a/app/src/main/res/values-zh-rCN/mozonline_strings.xml b/app/src/main/res/values-zh-rCN/mozonline_strings.xml new file mode 100644 index 000000000..d09a3e0b9 --- /dev/null +++ b/app/src/main/res/values-zh-rCN/mozonline_strings.xml @@ -0,0 +1,22 @@ + + + + + 关于您的权利 + + Mozilla Firefox 是一款自由开源软件,由来自世界各地成千上万的社区志愿者共同完成。以下几点您应该了解: + \n\n•Firefox 提供给您时依照的条款为 Mozilla 公共许可证(MPL)。这表示您可以使用、复制和向他人分发 Firefox。我们也非常欢迎您按自己的需要修改 Firefox 的源代码。Mozilla 公共许可证还授予您分发您自己修改过的软件版本的权利。 + \n•您没有获得 Mozilla 基金会或其他任何一方的商标权利或许可,这包括但不限于 Firefox 的名称或标志。有关商标的其他信息在:这里。 + \n•Firefox 的一些功能(例如崩溃报告器)使您可以向 Mozilla 提供反馈。提交反馈的同时,您授权 Mozilla 使用反馈信息改进产品、在其网站上发布反馈信息,以及分发反馈内容。 + \n•关于我们如何使用您通过 Firefox 提交给 Mozilla 的个人信息和反馈,请参见 Firefox 隐私权政策。 + + Mozilla 公共许可证(MPL) + + 这里 + + Firefox 隐私权政策 + + 同意并继续 + + 退出应用 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d7137ead5..fe6bc4d34 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -188,6 +188,8 @@ 字型调控 + + 定制阅读器视图 未知的 URL 规范,无法连接。 @@ -226,6 +228,12 @@ 详细了解 + + %s 上搜索 + + + 直接从地址栏搜索 + 新建 Firefox 标签页 @@ -898,7 +906,7 @@ 阻止 - 已允许 + 允许 被 Android 阻止 @@ -908,16 +916,22 @@ 关闭 - + 允许音频和视频 + + 允许音频和视频 在蜂窝数据下阻止音频和视频 将在 Wi-Fi 下播放音频和视频 - + 仅阻止音频 - + + 仅阻止音频 + 阻止音频和视频 + + 阻止音频和视频 开启 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index fc1d25a6f..c6029ca8b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -184,6 +184,8 @@ 外觀設定 + + 自訂閱讀模式 不認識的通訊協定,無法連線。 @@ -221,6 +223,11 @@ 了解更多 + + 搜尋 %s + + 從網址列直接搜尋 + 開新 Firefox 分頁 @@ -899,16 +906,22 @@ 關閉 - + 允許自動播放影音內容 + + 允許自動播放影音內容 僅在使用行動網路時封鎖影音內容 使用 Wi-Fi 網路時,仍會自動播放影音內容 - + 僅封鎖音訊 - + + 僅封鎖音訊 + 封鎖影音內容 + + 封鎖影音內容 開啟 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 169b32455..f8d09ea35 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -419,4 +419,10 @@ @color/primary_text_light_theme @color/primary_text_light_theme + + + #1415141A + + + @android:color/transparent diff --git a/app/src/main/res/values/mozonline_strings.xml b/app/src/main/res/values/mozonline_strings.xml new file mode 100644 index 000000000..89c9fbc4c --- /dev/null +++ b/app/src/main/res/values/mozonline_strings.xml @@ -0,0 +1,22 @@ + + + + + About your rights + + Mozilla Firefox is free and open source software, built by a community of thousands from all over the world. There are a few things you should know: + \n\n•Firefox is made available to you under the terms of the Mozilla Public License. This means you may use, copy and distribute Firefox to others. You are also welcome to modify the source code of Firefox as you want to meet your needs. The Mozilla Public License also gives you the right to distribute your modified versions. + \n•You are not granted any trademark rights or licenses to the trademarks of the Mozilla Foundation or any party, including without limitation the Firefox name or logo. Additional information on trademarks may be found here. + \n•Some features in Firefox, such as the Crash Reporter, give you the option to provide feedback to Mozilla. By choosing to submit feedback, you give Mozilla permission to use the feedback to improve its products, to publish the feedback on its websites, and to distribute the feedback. + \n•How we use your personal information and feedback submitted to Mozilla through Firefox is described in the Firefox Privacy Policy. + + Mozilla Public License + + found here + + Firefox Privacy Policy + + Agree and Continue + + Exit the App + \ No newline at end of file diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml index b5d9797de..c5a0879c4 100644 --- a/app/src/main/res/values/preference_keys.xml +++ b/app/src/main/res/values/preference_keys.xml @@ -127,6 +127,9 @@ pref_key_toolbar_bottom pref_key_strip_url + + pref_key_privacy_pop_window + pref_key_light_theme pref_key_dark_theme @@ -220,8 +223,6 @@ pref_key_migrating_from_fenix_tip pref_key_master_password_tip - pref_key_synced_tabs_tabs_tray - pref_key_debug_settings pref_key_open_tabs_count diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index acde8e7e8..974318c8c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -130,6 +130,8 @@ Edit bookmark Add-ons + + Extensions No add-ons here @@ -527,6 +529,10 @@ Other Bookmarks History + + New tab + + Find in page Synced tabs @@ -871,16 +877,22 @@ On Off - + Allow audio and video + + Allow audio and video Block audio and video on cellular data only Audio and video will play on Wi-Fi - + Block audio only - + + Block audio only + Block audio and video + + Block audio and video On diff --git a/app/src/main/res/xml/secret_settings_preferences.xml b/app/src/main/res/xml/secret_settings_preferences.xml index 7f9722266..c9a243d18 100644 --- a/app/src/main/res/xml/secret_settings_preferences.xml +++ b/app/src/main/res/xml/secret_settings_preferences.xml @@ -4,9 +4,4 @@ - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - diff --git a/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt b/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.kt new file mode 100644 index 000000000..a82099258 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/FenixApplicationTest.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 + +import androidx.test.core.app.ApplicationProvider +import mozilla.components.service.glean.testing.GleanTestRule +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.PerfStartup +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class FenixApplicationTest { + + @get:Rule val gleanTestRule = GleanTestRule(ApplicationProvider.getApplicationContext()) + + private lateinit var application: FenixApplication + + @Before + fun setUp() { + application = ApplicationProvider.getApplicationContext() + } + + @Ignore("See https://github.com/mozilla-mobile/fenix/issues/18102") + @Test + fun `GIVEN onCreate is called THEN the duration is measured`() { + // application.onCreate is called before the test as part of test set up: + // https://robolectric.blogspot.com/2013/04/the-test-lifecycle-in-20.html + assertTrue(PerfStartup.applicationOnCreate.testHasValue()) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/HomeActivityTest.kt b/app/src/test/java/org/mozilla/fenix/HomeActivityTest.kt index 9d83d5360..9c086a9f5 100644 --- a/app/src/test/java/org/mozilla/fenix/HomeActivityTest.kt +++ b/app/src/test/java/org/mozilla/fenix/HomeActivityTest.kt @@ -6,10 +6,12 @@ package org.mozilla.fenix import android.content.Intent import android.os.Bundle +import androidx.test.core.app.ApplicationProvider import io.mockk.every import io.mockk.mockk import io.mockk.spyk import io.mockk.verify +import mozilla.components.service.glean.testing.GleanTestRule import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.utils.toSafeIntent import org.junit.Assert.assertEquals @@ -18,8 +20,10 @@ import org.junit.Assert.assertNotEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.HomeActivity.Companion.PRIVATE_BROWSING_MODE import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager @@ -28,10 +32,13 @@ import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.settings import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.utils.Settings +import org.robolectric.Robolectric @RunWith(FenixRobolectricTestRunner::class) class HomeActivityTest { + @get:Rule val gleanTestRule = GleanTestRule(ApplicationProvider.getApplicationContext()) + private lateinit var activity: HomeActivity @Before @@ -131,4 +138,20 @@ class HomeActivityTest { assertFalse(activity.isActivityColdStarted(startingIntent, Bundle())) } + + @Test + fun `WHEN onCreate is called THEN the duration is measured`() { + assertFalse(PerfStartup.homeActivityOnCreate.testHasValue()) // sanity check. + + // For some reason, the androidx replacement for this method, ActivityScenario, fails so we + // use the old Robolectric version. Perhaps it's because it forces the Activity to the + // RESUMED state (unlike Robolectric where we can get to CREATED) so not enough code is + // mocked for that to work. + // + // There are various exceptions thrown on background threads when this test runs but it + // doesn't seem to impact correctness so we ignore them. + Robolectric.buildActivity(HomeActivity::class.java) + .create() + assertTrue(PerfStartup.homeActivityOnCreate.testHasValue()) + } } diff --git a/app/src/test/java/org/mozilla/fenix/TelemetryMiddlewareTest.kt b/app/src/test/java/org/mozilla/fenix/TelemetryMiddlewareTest.kt index 37fb3c1af..3f086f68f 100644 --- a/app/src/test/java/org/mozilla/fenix/TelemetryMiddlewareTest.kt +++ b/app/src/test/java/org/mozilla/fenix/TelemetryMiddlewareTest.kt @@ -4,21 +4,33 @@ package org.mozilla.fenix +import androidx.test.core.app.ApplicationProvider import io.mockk.mockk import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestCoroutineDispatcher +import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.DownloadAction +import mozilla.components.browser.state.action.EngineAction import mozilla.components.browser.state.action.TabListAction +import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.LoadRequestState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore +import mozilla.components.service.glean.testing.GleanTestRule +import mozilla.components.support.base.android.Clock import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.mock import mozilla.components.support.test.robolectric.testContext +import mozilla.components.support.test.rule.MainCoroutineRule +import org.junit.After +import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.components.metrics.Event @@ -26,8 +38,10 @@ import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.utils.Settings +import org.mozilla.fenix.GleanMetrics.Engine as EngineMetrics @RunWith(FenixRobolectricTestRunner::class) +@ExperimentalCoroutinesApi class TelemetryMiddlewareTest { private lateinit var store: BrowserStore @@ -35,18 +49,37 @@ class TelemetryMiddlewareTest { private lateinit var telemetryMiddleware: TelemetryMiddleware private lateinit var metrics: MetricController private lateinit var adsTelemetry: AdsTelemetry + private val testDispatcher = TestCoroutineDispatcher() + + @get:Rule + val coroutinesTestRule = MainCoroutineRule(testDispatcher) + + @get:Rule + val gleanRule = GleanTestRule(ApplicationProvider.getApplicationContext()) + + private val clock = FakeClock() @Before fun setUp() { + Clock.delegate = clock + settings = Settings(testContext) - metrics = mockk() + metrics = mockk(relaxed = true) adsTelemetry = mockk() telemetryMiddleware = TelemetryMiddleware( settings, adsTelemetry, metrics ) - store = BrowserStore(middleware = listOf(telemetryMiddleware)) + store = BrowserStore( + middleware = listOf(telemetryMiddleware) + EngineMiddleware.create(engine = mockk(), sessionLookup = { null }), + initialState = BrowserState() + ) + } + + @After + fun tearDown() { + Clock.reset() } @Test @@ -55,6 +88,7 @@ class TelemetryMiddlewareTest { store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org"))).joinBlocking() assertEquals(1, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } } @Test @@ -63,6 +97,7 @@ class TelemetryMiddlewareTest { store.dispatch(TabListAction.AddTabAction(createTab("https://mozilla.org", private = true))).joinBlocking() assertEquals(0, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveNoOpenTabs) } } @Test @@ -76,6 +111,7 @@ class TelemetryMiddlewareTest { ).joinBlocking() assertEquals(2, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } } @Test @@ -87,9 +123,11 @@ class TelemetryMiddlewareTest { ) ).joinBlocking() assertEquals(2, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } store.dispatch(TabListAction.RemoveTabAction("1")).joinBlocking() assertEquals(1, settings.openTabsCount) + verify(exactly = 2) { metrics.track(Event.HaveOpenTabs) } } @Test @@ -101,9 +139,11 @@ class TelemetryMiddlewareTest { ) ).joinBlocking() assertEquals(2, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } store.dispatch(TabListAction.RemoveAllTabsAction).joinBlocking() assertEquals(0, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveNoOpenTabs) } } @Test @@ -116,9 +156,11 @@ class TelemetryMiddlewareTest { ) ).joinBlocking() assertEquals(2, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } store.dispatch(TabListAction.RemoveAllNormalTabsAction).joinBlocking() assertEquals(0, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveNoOpenTabs) } } @Test @@ -131,6 +173,7 @@ class TelemetryMiddlewareTest { store.dispatch(TabListAction.RestoreAction(tabsToRestore)).joinBlocking() assertEquals(2, settings.openTabsCount) + verify(exactly = 1) { metrics.track(Event.HaveOpenTabs) } } @Test @@ -139,9 +182,11 @@ class TelemetryMiddlewareTest { store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking() verify(exactly = 0) { metrics.track(Event.UriOpened) } + verify(exactly = 0) { metrics.track(Event.NormalAndPrivateUriOpened) } store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking() verify(exactly = 1) { metrics.track(Event.UriOpened) } + verify(exactly = 1) { metrics.track(Event.NormalAndPrivateUriOpened) } } @Test @@ -150,9 +195,11 @@ class TelemetryMiddlewareTest { store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking() store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, true)).joinBlocking() verify(exactly = 0) { metrics.track(Event.UriOpened) } + verify(exactly = 0) { metrics.track(Event.NormalAndPrivateUriOpened) } store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking() verify(exactly = 0) { metrics.track(Event.UriOpened) } + verify(exactly = 1) { metrics.track(Event.NormalAndPrivateUriOpened) } } @Test @@ -225,4 +272,125 @@ class TelemetryMiddlewareTest { verify { metrics.track(Event.DownloadAdded) } } + + @Test + fun `WHEN foreground tab getting killed THEN middleware counts it`() { + store.dispatch(TabListAction.RestoreAction( + listOf( + createTab("https://www.mozilla.org", id = "foreground"), + createTab("https://getpocket.com", id = "background_pocket"), + createTab("https://theverge.com", id = "background_verge") + ), + selectedTabId = "foreground" + )).joinBlocking() + + Assert.assertFalse(EngineMetrics.tabKills["foreground"].testHasValue()) + Assert.assertFalse(EngineMetrics.tabKills["background"].testHasValue()) + + store.dispatch( + EngineAction.KillEngineSessionAction("foreground") + ).joinBlocking() + + Assert.assertTrue(EngineMetrics.tabKills["foreground"].testHasValue()) + } + + @Test + fun `WHEN background tabs getting killed THEN middleware counts it`() { + store.dispatch(TabListAction.RestoreAction( + listOf( + createTab("https://www.mozilla.org", id = "foreground"), + createTab("https://getpocket.com", id = "background_pocket"), + createTab("https://theverge.com", id = "background_verge") + ), + selectedTabId = "foreground" + )).joinBlocking() + + Assert.assertFalse(EngineMetrics.tabKills["foreground"].testHasValue()) + Assert.assertFalse(EngineMetrics.tabKills["background"].testHasValue()) + + store.dispatch( + EngineAction.KillEngineSessionAction("background_pocket") + ).joinBlocking() + + Assert.assertFalse(EngineMetrics.tabKills["foreground"].testHasValue()) + Assert.assertTrue(EngineMetrics.tabKills["background"].testHasValue()) + assertEquals(1, EngineMetrics.tabKills["background"].testGetValue()) + + store.dispatch( + EngineAction.KillEngineSessionAction("background_verge") + ).joinBlocking() + + Assert.assertFalse(EngineMetrics.tabKills["foreground"].testHasValue()) + Assert.assertTrue(EngineMetrics.tabKills["background"].testHasValue()) + assertEquals(2, EngineMetrics.tabKills["background"].testGetValue()) + } + + @Test + fun `WHEN foreground tab gets killed THEN middleware records foreground age`() { + store.dispatch(TabListAction.RestoreAction( + listOf( + createTab("https://www.mozilla.org", id = "foreground"), + createTab("https://getpocket.com", id = "background_pocket"), + createTab("https://theverge.com", id = "background_verge") + ), + selectedTabId = "foreground" + )).joinBlocking() + + clock.elapsedTime = 100 + + store.dispatch(EngineAction.LinkEngineSessionAction( + sessionId = "foreground", + engineSession = mock() + )).joinBlocking() + + Assert.assertFalse(EngineMetrics.killForegroundAge.testHasValue()) + Assert.assertFalse(EngineMetrics.killBackgroundAge.testHasValue()) + + clock.elapsedTime = 500 + + store.dispatch( + EngineAction.KillEngineSessionAction("foreground") + ).joinBlocking() + + Assert.assertTrue(EngineMetrics.killForegroundAge.testHasValue()) + Assert.assertFalse(EngineMetrics.killBackgroundAge.testHasValue()) + assertEquals(400, EngineMetrics.killForegroundAge.testGetValue()) + } + + @Test + fun `WHEN background tab gets killed THEN middleware records background age`() { + store.dispatch(TabListAction.RestoreAction( + listOf( + createTab("https://www.mozilla.org", id = "foreground"), + createTab("https://getpocket.com", id = "background_pocket"), + createTab("https://theverge.com", id = "background_verge") + ), + selectedTabId = "foreground" + )).joinBlocking() + + clock.elapsedTime = 100 + + store.dispatch(EngineAction.LinkEngineSessionAction( + sessionId = "background_pocket", + engineSession = mock() + )).joinBlocking() + + clock.elapsedTime = 700 + + Assert.assertFalse(EngineMetrics.killForegroundAge.testHasValue()) + Assert.assertFalse(EngineMetrics.killBackgroundAge.testHasValue()) + + store.dispatch( + EngineAction.KillEngineSessionAction("background_pocket") + ).joinBlocking() + + Assert.assertTrue(EngineMetrics.killBackgroundAge.testHasValue()) + Assert.assertFalse(EngineMetrics.killForegroundAge.testHasValue()) + assertEquals(600, EngineMetrics.killBackgroundAge.testGetValue()) + } +} + +private class FakeClock : Clock.Delegate { + var elapsedTime: Long = 0 + override fun elapsedRealtime(): Long = elapsedTime } diff --git a/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt new file mode 100644 index 000000000..c0d486e17 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/browser/BaseBrowserFragmentTest.kt @@ -0,0 +1,217 @@ +/* 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.browser + +import android.content.Context +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.spyk +import io.mockk.verify +import junit.framework.TestCase.assertFalse +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.content.DownloadState +import mozilla.components.browser.state.state.createTab +import mozilla.components.concept.engine.EngineView +import mozilla.components.feature.contextmenu.ContextMenuCandidate +import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior +import mozilla.components.feature.sitepermissions.SitePermissions +import mozilla.components.support.test.robolectric.testContext +import mozilla.components.ui.widgets.VerticalSwipeRefreshLayout +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class BaseBrowserFragmentTest { + private lateinit var fragment: TestBaseBrowserFragment + private lateinit var swipeRefreshLayout: VerticalSwipeRefreshLayout + private lateinit var engineView: EngineView + + @Before + fun setup() { + fragment = spyk(TestBaseBrowserFragment()) + swipeRefreshLayout = mockk(relaxed = true) + engineView = mockk(relaxed = true) + every { fragment.isAdded } returns true + every { fragment.activity } returns mockk() + every { fragment.requireContext() } returns testContext + every { fragment.getEngineView() } returns engineView + every { fragment.getSwipeRefreshLayout() } returns swipeRefreshLayout + every { swipeRefreshLayout.layoutParams } returns mockk(relaxed = true) + } + + @Test + fun `initializeEngineView should setDynamicToolbarMaxHeight to 0 if top toolbar is forced for a11y`() { + every { testContext.settings().shouldUseBottomToolbar } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + fragment.initializeEngineView(13) + + verify { engineView.setDynamicToolbarMaxHeight(0) } + } + + @Test + fun `initializeEngineView should setDynamicToolbarMaxHeight to 0 if bottom toolbar is forced for a11y`() { + every { testContext.settings().shouldUseBottomToolbar } returns true + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + fragment.initializeEngineView(13) + + verify { engineView.setDynamicToolbarMaxHeight(0) } + } + + @Test + fun `initializeEngineView should setDynamicToolbarMaxHeight to toolbar height if dynamic toolbar is enabled`() { + every { testContext.settings().shouldUseFixedTopToolbar } returns false + every { testContext.settings().isDynamicToolbarEnabled } returns true + + fragment.initializeEngineView(13) + + verify { engineView.setDynamicToolbarMaxHeight(13) } + } + + @Test + fun `initializeEngineView should setDynamicToolbarMaxHeight to 0 if dynamic toolbar is disabled`() { + every { testContext.settings().shouldUseFixedTopToolbar } returns false + every { testContext.settings().isDynamicToolbarEnabled } returns false + + fragment.initializeEngineView(13) + + verify { engineView.setDynamicToolbarMaxHeight(0) } + } + + @Test + fun `initializeEngineView should set EngineViewBrowserToolbarBehavior when dynamic toolbar is enabled`() { + every { testContext.settings().shouldUseFixedTopToolbar } returns false + every { testContext.settings().isDynamicToolbarEnabled } returns true + val params: CoordinatorLayout.LayoutParams = mockk(relaxed = true) + every { params.behavior } returns mockk(relaxed = true) + every { swipeRefreshLayout.layoutParams } returns params + val behavior = slot() + + fragment.initializeEngineView(13) + + // EngineViewBrowserToolbarBehavior constructor parameters are not properties, we cannot check them. + // Ensure just that the right behavior is set. + verify { params.behavior = capture(behavior) } + } + + @Test + fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin when using bottom toolbar`() { + every { testContext.settings().isDynamicToolbarEnabled } returns false + every { testContext.settings().shouldUseBottomToolbar } returns true + + fragment.initializeEngineView(13) + + verify { (swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 13 } + } + + @Test + fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin if top toolbar is forced for a11y`() { + every { testContext.settings().shouldUseBottomToolbar } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + fragment.initializeEngineView(13) + + verify { (swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 13 } + } + + @Test + fun `initializeEngineView should set toolbar height as EngineView parent's bottom margin if bottom toolbar is forced for a11y`() { + every { testContext.settings().shouldUseBottomToolbar } returns true + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + fragment.initializeEngineView(13) + + verify { (swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 13 } + } + + @Test + fun `WHEN status is equals to FAILED or COMPLETED and it is the same tab then shouldShowCompletedDownloadDialog will be true`() { + every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "1" + ) + + val status = DownloadState.Status.values() + .filter { it == DownloadState.Status.COMPLETED && it == DownloadState.Status.FAILED } + + status.forEach { + val result = + fragment.shouldShowCompletedDownloadDialog(download, it) + + assertTrue(result) + } + } + + @Test + fun `WHEN status is different from FAILED or COMPLETED then shouldShowCompletedDownloadDialog will be false`() { + every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "1" + ) + + val status = DownloadState.Status.values() + .filter { it != DownloadState.Status.COMPLETED && it != DownloadState.Status.FAILED } + + status.forEach { + val result = + fragment.shouldShowCompletedDownloadDialog(download, it) + + assertFalse(result) + } + } + + @Test + fun `WHEN the tab is different from the initial one then shouldShowCompletedDownloadDialog will be false`() { + every { fragment.getCurrentTab() } returns createTab(id = "1", url = "") + + val download = DownloadState( + url = "", + sessionId = "2" + ) + + val status = DownloadState.Status.values() + .filter { it != DownloadState.Status.COMPLETED && it != DownloadState.Status.FAILED } + + status.forEach { + val result = + fragment.shouldShowCompletedDownloadDialog(download, it) + + assertFalse(result) + } + } +} + +@ExperimentalCoroutinesApi +private class TestBaseBrowserFragment : BaseBrowserFragment() { + override fun getContextMenuCandidates( + context: Context, + view: View + ): List { + // no-op + return emptyList() + } + + override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) { + // no-op + } + + override fun navToTrackingProtectionPanel(tab: SessionState) { + // no-op + } +} diff --git a/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt index dd4774766..cd32d5ee0 100644 --- a/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/browser/BrowserFragmentTest.kt @@ -347,44 +347,14 @@ class BrowserFragmentTest { } @Test - fun `WHEN fragment is not attached THEN toolbar invalidation does nothing`() { + fun `WHEN fragment configuration changed THEN menu is dismissed`() { val browserToolbarView: BrowserToolbarView = mockk(relaxed = true) - val browserToolbar: BrowserToolbar = mockk(relaxed = true) - val toolbarIntegration: ToolbarIntegration = mockk(relaxed = true) - every { browserToolbarView.view } returns browserToolbar - every { browserToolbarView.toolbarIntegration } returns toolbarIntegration every { browserFragment.context } returns null browserFragment._browserToolbarView = browserToolbarView - browserFragment.safeInvalidateBrowserToolbarView() - verify(exactly = 0) { browserToolbar.invalidateActions() } - verify(exactly = 0) { toolbarIntegration.invalidateMenu() } - } + browserFragment.onConfigurationChanged(mockk(relaxed = true)) - @Test - @Suppress("TooGenericExceptionCaught") - fun `WHEN fragment is attached and toolbar view is null THEN toolbar invalidation is safe`() { - every { browserFragment.context } returns mockk(relaxed = true) - try { - browserFragment.safeInvalidateBrowserToolbarView() - } catch (e: Exception) { - fail("Exception thrown when invalidating toolbar") - } - } - - @Test - fun `WHEN fragment and view are attached THEN toolbar invalidation is triggered`() { - val browserToolbarView: BrowserToolbarView = mockk(relaxed = true) - val browserToolbar: BrowserToolbar = mockk(relaxed = true) - val toolbarIntegration: ToolbarIntegration = mockk(relaxed = true) - every { browserToolbarView.view } returns browserToolbar - every { browserToolbarView.toolbarIntegration } returns toolbarIntegration - every { browserFragment.context } returns mockk(relaxed = true) - browserFragment._browserToolbarView = browserToolbarView - browserFragment.safeInvalidateBrowserToolbarView() - - verify(exactly = 1) { browserToolbar.invalidateActions() } - verify(exactly = 1) { toolbarIntegration.invalidateMenu() } + verify(exactly = 1) { browserToolbarView.dismissMenu() } } private fun addAndSelectTab(tab: TabSessionState) { diff --git a/app/src/test/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserverTest.kt b/app/src/test/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserverTest.kt index ce0cf7f01..3645bf604 100644 --- a/app/src/test/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserverTest.kt +++ b/app/src/test/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserverTest.kt @@ -11,7 +11,9 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry import androidx.navigation.NavController import io.mockk.every +import io.mockk.just import io.mockk.mockk +import io.mockk.runs import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -22,12 +24,17 @@ import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.robolectric.testContext import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.browser.infobanner.DynamicInfoBanner +import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixRobolectricTestRunner import org.mozilla.fenix.utils.Settings @@ -43,7 +50,7 @@ class OpenInAppOnboardingObserverTest { private lateinit var appLinksUseCases: AppLinksUseCases private lateinit var context: Context private lateinit var container: ViewGroup - private lateinit var infoBanner: InfoBanner + private lateinit var infoBanner: DynamicInfoBanner private val testDispatcher = TestCoroutineDispatcher() @@ -73,7 +80,8 @@ class OpenInAppOnboardingObserverTest { navController = navigationController, settings = settings, appLinksUseCases = appLinksUseCases, - container = container + container = container, + shouldScrollWithTopToolbar = true )) every { openInAppOnboardingObserver.createInfoBanner() } returns infoBanner } @@ -138,6 +146,7 @@ class OpenInAppOnboardingObserverTest { every { settings.openLinksInExternalApp } returns false every { settings.shouldShowOpenInAppCfr } returns true every { appLinksUseCases.appLinkRedirect.invoke(any()).hasExternalApp() } returns true + every { context.components.analytics.metrics.track(any()) } just runs store.dispatch(ContentAction.UpdateLoadingStateAction("1", true)).joinBlocking() openInAppOnboardingObserver.start() @@ -153,6 +162,24 @@ class OpenInAppOnboardingObserverTest { verify(exactly = 1) { infoBanner.dismiss() } } + @Test + fun `GIVEN a observer WHEN createInfoBanner() THEN the scrollWithTopToolbar is passed to the DynamicInfoBanner`() { + // Mockk currently doesn't support verifying constructor parameters + // But we can check the values found in the constructed objects + + openInAppOnboardingObserver = spyk(OpenInAppOnboardingObserver( + testContext, mockk(), mockk(), mockk(), mockk(), mockk(), mockk(), shouldScrollWithTopToolbar = true + )) + val banner1 = openInAppOnboardingObserver.createInfoBanner() + assertTrue(banner1.shouldScrollWithTopToolbar) + + openInAppOnboardingObserver = spyk(OpenInAppOnboardingObserver( + testContext, mockk(), mockk(), mockk(), mockk(), mockk(), mockk(), shouldScrollWithTopToolbar = false + )) + val banner2 = openInAppOnboardingObserver.createInfoBanner() + assertFalse(banner2.shouldScrollWithTopToolbar) + } + internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner { val lifecycleRegistry = LifecycleRegistry(this).apply { currentState = initialState diff --git a/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehaviorTest.kt b/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehaviorTest.kt new file mode 100644 index 000000000..69fd5d8ca --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerBehaviorTest.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.browser.infobanner + +import android.view.View +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import mozilla.components.browser.toolbar.BrowserToolbar +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class DynamicInfoBannerBehaviorTest { + @Test + fun `layoutDependsOn should not do anything if not for BrowserToolbar as a dependency`() { + val behavior = spyk(DynamicInfoBannerBehavior(mockk(), null)) + + behavior.layoutDependsOn(mockk(), mockk(), mockk()) + + verify(exactly = 0) { behavior.toolbarHeight } + verify(exactly = 0) { behavior.toolbarHeight = any() } + verify(exactly = 0) { behavior.setBannerYTranslation(any(), any()) } + } + + @Test + fun `layoutDependsOn should update toolbarHeight and translate the banner`() { + val behavior = spyk(DynamicInfoBannerBehavior(mockk(), null)) + val banner: View = mockk(relaxed = true) + val toolbar: BrowserToolbar = mockk { + every { height } returns 99 + every { translationY } returns -33f + } + + assertEquals(0, behavior.toolbarHeight) + + behavior.layoutDependsOn(mockk(), banner, toolbar) + + assertEquals(99, behavior.toolbarHeight) + verify { behavior.setBannerYTranslation(banner, -33f) } + } + + @Test + fun `onDependentViewChanged should translate the banner`() { + val behavior = spyk(DynamicInfoBannerBehavior(mockk(), null)) + val banner: View = mockk(relaxed = true) + val toolbar: BrowserToolbar = mockk { + every { height } returns 50 + every { translationY } returns -23f + } + + behavior.layoutDependsOn(mockk(), banner, toolbar) + + verify { behavior.setBannerYTranslation(banner, -23f) } + } + + @Test + fun `setBannerYTranslation should set banner translation to be toolbarHeight + it's translation`() { + val behavior = spyk(DynamicInfoBannerBehavior(mockk(), null)) + val banner: View = mockk(relaxed = true) + behavior.toolbarHeight = 30 + + behavior.setBannerYTranslation(banner, -20f) + + verify { banner.translationY = 10f } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerTest.kt b/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerTest.kt new file mode 100644 index 000000000..d894071ab --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/browser/infobanner/DynamicInfoBannerTest.kt @@ -0,0 +1,39 @@ +/* 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.browser.infobanner + +import androidx.coordinatorlayout.widget.CoordinatorLayout +import io.mockk.spyk +import mozilla.components.support.test.robolectric.testContext +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner + +@RunWith(FenixRobolectricTestRunner::class) +class DynamicInfoBannerTest { + @Test + fun `showBanner should set DynamicInfoBannerBehavior as behavior if scrollWithTopToolbar`() { + val banner = spyk(DynamicInfoBanner( + testContext, CoordinatorLayout(testContext), true, "", "" + )) + + banner.showBanner() + + assertTrue((banner.bannerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior is DynamicInfoBannerBehavior) + } + + @Test + fun `showBanner should not set a behavior if not scrollWithTopToolbar`() { + val banner = spyk(DynamicInfoBanner( + testContext, CoordinatorLayout(testContext), false, "", "" + )) + + banner.showBanner() + + assertNull((banner.bannerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt b/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt index 5af2f79a6..5d04f96b6 100644 --- a/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/collections/DefaultCollectionCreationControllerTest.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestCoroutineScope import kotlinx.coroutines.test.runBlockingTest import mozilla.components.browser.state.action.TabListAction -import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.tab.collections.TabCollection @@ -80,8 +79,8 @@ class DefaultCollectionCreationControllerTest { ).joinBlocking() val tabs = listOf( - Tab("session-1", "", "", "", mediaState = MediaState.State.NONE), - Tab("null-session", "", "", "", mediaState = MediaState.State.NONE) + Tab("session-1", "", "", ""), + Tab("null-session", "", "", "") ) controller.saveCollectionName(tabs, "name") @@ -173,7 +172,7 @@ class DefaultCollectionCreationControllerTest { ).joinBlocking() val tabs = listOf( - Tab("session-1", "", "", "", mediaState = MediaState.State.NONE) + Tab("session-1", "", "", "") ) val collection = mockk() diff --git a/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt b/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt index 577790914..dfc88b0e9 100644 --- a/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt +++ b/app/src/test/java/org/mozilla/fenix/components/TestComponents.kt @@ -16,6 +16,7 @@ class TestComponents(private val context: Context) : Components(context) { } override val services by lazy { Services(context, backgroundServices.accountManager) } override val core by lazy { TestCore(context, analytics.crashReporter) } + @Suppress("Deprecation") override val useCases by lazy { UseCases( context, diff --git a/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt index 97934c541..be97fd8a3 100644 --- a/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/metrics/MetricControllerTest.kt @@ -10,7 +10,12 @@ import io.mockk.impl.annotations.MockK import io.mockk.mockk import io.mockk.verify import io.mockk.verifyAll +import mozilla.components.feature.top.sites.facts.TopSitesFacts +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact import mozilla.components.support.base.log.logger.Logger +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -167,4 +172,59 @@ class MetricControllerTest { controller.track(Event.TabMediaPause) verify { marketingService1.track(Event.TabMediaPause) } } + + @Test + fun `topsites fact should convert to the right events`() { + var enabled = true + val controller = ReleaseMetricController( + services = listOf(dataService1), + isDataTelemetryEnabled = { enabled }, + isMarketingDataTelemetryEnabled = { enabled } + ) + + var fact = Fact( + Component.FEATURE_TOP_SITES, + Action.INTERACTION, + TopSitesFacts.Items.COUNT, + "1" + ) + + assertEquals(controller.factToEvent(fact), Event.HaveTopSites) + + fact = Fact( + Component.FEATURE_TOP_SITES, + Action.INTERACTION, + TopSitesFacts.Items.COUNT, + "0" + ) + + assertEquals(controller.factToEvent(fact), Event.HaveNoTopSites) + + fact = Fact( + Component.FEATURE_TOP_SITES, + Action.INTERACTION, + TopSitesFacts.Items.COUNT, + "10" + ) + + assertEquals(controller.factToEvent(fact), Event.HaveTopSites) + + fact = Fact( + Component.FEATURE_TOP_SITES, + Action.INTERACTION, + TopSitesFacts.Items.COUNT, + "-4" + ) + + assertEquals(controller.factToEvent(fact), Event.HaveNoTopSites) + + fact = Fact( + Component.FEATURE_TOP_SITES, + Action.INTERACTION, + TopSitesFacts.Items.COUNT, + "test" + ) + + assertEquals(controller.factToEvent(fact), Event.HaveNoTopSites) + } } diff --git a/app/src/test/java/org/mozilla/fenix/components/settings/sitepermissions/ExtensionsTest.kt b/app/src/test/java/org/mozilla/fenix/components/settings/sitepermissions/ExtensionsTest.kt new file mode 100644 index 000000000..0c91f91d0 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/settings/sitepermissions/ExtensionsTest.kt @@ -0,0 +1,45 @@ +/* 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.settings.sitepermissions + +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.createTab +import mozilla.components.browser.state.store.BrowserStore +import org.junit.Test +import org.junit.runner.RunWith +import org.mozilla.fenix.components.Components +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.settings.sitepermissions.tryReloadTabBy + +@RunWith(FenixRobolectricTestRunner::class) +class ExtensionsTest { + + @Test + fun `tryReloadTabBy reloads latest tab matching origin`() { + val store = BrowserStore( + BrowserState(tabs = listOf( + createTab(id = "1", url = "https://www.mozilla.org/1", lastAccess = 1), + createTab(id = "2", url = "https://www.mozilla.org/2", lastAccess = 2), + createTab(id = "3", url = "https://www.firefox.com") + ) + ) + ) + + val components: Components = mockk(relaxed = true) + every { components.core.store } returns store + + components.tryReloadTabBy("www.getpocket.com") + verify(exactly = 0) { components.useCases.sessionUseCases.reload(any()) } + + components.tryReloadTabBy("www.mozilla.org") + verify { components.useCases.sessionUseCases.reload("2") } + + components.tryReloadTabBy("www.firefox.com") + verify { components.useCases.sessionUseCases.reload("3") } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt new file mode 100644 index 000000000..727f4f6a8 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/BrowserToolbarViewTest.kt @@ -0,0 +1,279 @@ +/* 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.toolbar + +import androidx.coordinatorlayout.widget.CoordinatorLayout +import io.mockk.confirmVerified + +import io.mockk.every +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.support.test.robolectric.testContext +import org.junit.Test +import org.junit.runner.RunWith +import mozilla.components.browser.toolbar.BrowserToolbar +import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Before +import org.mozilla.fenix.ext.settings +import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import mozilla.components.browser.toolbar.behavior.ToolbarPosition as MozacToolbarPosition + +@ExperimentalCoroutinesApi +@RunWith(FenixRobolectricTestRunner::class) +class BrowserToolbarViewTest { + private lateinit var toolbarView: BrowserToolbarView + private lateinit var toolbar: BrowserToolbar + private lateinit var behavior: BrowserToolbarBehavior + + @Before + fun `setup`() { + toolbar = BrowserToolbar(testContext) + toolbar.layoutParams = CoordinatorLayout.LayoutParams(100, 100) + behavior = spyk(BrowserToolbarBehavior(testContext, null, MozacToolbarPosition.BOTTOM)) + (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior + + toolbarView = BrowserToolbarView( + container = CoordinatorLayout(testContext), + toolbarPosition = ToolbarPosition.BOTTOM, + interactor = mockk(), + customTabSession = mockk(relaxed = true), + lifecycleOwner = mockk() + ) + + toolbarView.view = toolbar + } + + @Test + fun `setToolbarBehavior(false) should setDynamicToolbarBehavior if no a11y, bottom toolbar is dynamic and the tab is not for a PWA or TWA`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + } + + @Test + fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is not set as dynamic`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns false + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic but the tab is for a PWA or TWA`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns true + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(false) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic tab is not for a PWA or TWA but a11y is enabled`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed bottom toolbar is dynamic, the tab is not for a PWA or TWA and a11y is disabled`() { + // All intrinsic checks are met but the method was called with `shouldDisableScroll` = true + + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is not set as dynamic`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns false + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic but the tab is for a PWA or TWA`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns true + every { testContext.settings().shouldUseFixedTopToolbar } returns false + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed if bottom toolbar is dynamic, the tab is for a PWA or TWA and a11 is enabled`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.BOTTOM + every { testContext.settings().isDynamicToolbarEnabled } returns true + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + toolbarViewSpy.setToolbarBehavior(false) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if shouldUseFixedTopToolbar`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.TOP + every { testContext.settings().shouldUseFixedTopToolbar } returns true + + toolbarViewSpy.setToolbarBehavior(true) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if it is not dynamic`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.TOP + every { testContext.settings().isDynamicToolbarEnabled } returns false + + toolbarViewSpy.setToolbarBehavior(true) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(true) should expandToolbarAndMakeItFixed for top toolbar if shouldDisableScroll`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.TOP + + toolbarViewSpy.setToolbarBehavior(true) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `setToolbarBehavior(false) should setDynamicToolbarBehavior for top toolbar`() { + val toolbarViewSpy = spyk(toolbarView) + every { testContext.settings().toolbarPosition } returns ToolbarPosition.TOP + every { testContext.settings().shouldUseFixedTopToolbar } returns true + every { testContext.settings().isDynamicToolbarEnabled } returns true + + toolbarViewSpy.setToolbarBehavior(true) + + verify { toolbarViewSpy.expandToolbarAndMakeItFixed() } + } + + @Test + fun `expandToolbarAndMakeItFixed should expand the toolbar and and disable the dynamic behavior`() { + val toolbarViewSpy = spyk(toolbarView) + + assertNotNull((toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior) + + toolbarViewSpy.expandToolbarAndMakeItFixed() + + verify { toolbarViewSpy.expand() } + assertNull((toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior) + } + + @Test + fun `setDynamicToolbarBehavior should set a BrowserToolbarBehavior for the bottom toolbar`() { + val toolbarViewSpy = spyk(toolbarView) + (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null + + toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM) + + assertNotNull((toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior) + } + + @Test + fun `setDynamicToolbarBehavior should set a BrowserToolbarBehavior for the top toolbar`() { + val toolbarViewSpy = spyk(toolbarView) + (toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior = null + + toolbarViewSpy.setDynamicToolbarBehavior(MozacToolbarPosition.TOP) + + assertNotNull((toolbar.layoutParams as CoordinatorLayout.LayoutParams).behavior) + } + + @Test + fun `expand should not do anything if isPwaTabOrTwaTab`() { + val toolbarViewSpy = spyk(toolbarView) + every { toolbarViewSpy.isPwaTabOrTwaTab } returns true + + toolbarViewSpy.expand() + + verify { toolbarViewSpy.expand() } + verify { toolbarViewSpy.isPwaTabOrTwaTab } + // verify that no other interactions than the expected ones took place + confirmVerified(toolbarViewSpy) + } + + @Test + fun `expand should call forceExpand if not isPwaTabOrTwaTab`() { + val toolbarViewSpy = spyk(toolbarView) + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + + toolbarViewSpy.expand() + + verify { behavior.forceExpand(toolbar) } + } + + @Test + fun `collapse should not do anything if isPwaTabOrTwaTab`() { + val toolbarViewSpy = spyk(toolbarView) + every { toolbarViewSpy.isPwaTabOrTwaTab } returns true + + toolbarViewSpy.collapse() + + verify { toolbarViewSpy.collapse() } + verify { toolbarViewSpy.isPwaTabOrTwaTab } + // verify that no other interactions than the expected ones took place + confirmVerified(toolbarViewSpy) + } + + @Test + fun `collapse should call forceExpand if not isPwaTabOrTwaTab`() { + val toolbarViewSpy = spyk(toolbarView) + every { toolbarViewSpy.isPwaTabOrTwaTab } returns false + + toolbarViewSpy.collapse() + + verify { behavior.forceCollapse(toolbar) } + } +} diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt index ab7cacbbc..546ae4fea 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarControllerTest.kt @@ -15,17 +15,19 @@ import io.mockk.just import io.mockk.mockk import io.mockk.slot import io.mockk.verify -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.ContentAction +import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.state.BrowserState +import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineView import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases +import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.top.sites.TopSitesUseCases +import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.libstate.ext.waitUntilIdle import mozilla.components.support.test.middleware.CaptureActionsMiddleware import mozilla.components.ui.tabcounter.TabCounterMenu @@ -61,17 +63,11 @@ class DefaultBrowserToolbarControllerTest { private lateinit var onTabCounterClicked: () -> Unit @RelaxedMockK - private lateinit var onCloseTab: (Session) -> Unit - - @RelaxedMockK - private lateinit var sessionManager: SessionManager + private lateinit var onCloseTab: (SessionState) -> Unit @MockK(relaxUnitFun = true) private lateinit var engineView: EngineView - @MockK - private lateinit var currentSession: Session - @RelaxedMockK private lateinit var metrics: MetricController @@ -81,6 +77,9 @@ class DefaultBrowserToolbarControllerTest { @RelaxedMockK private lateinit var sessionUseCases: SessionUseCases + @RelaxedMockK + private lateinit var tabsUseCases: TabsUseCases + @RelaxedMockK private lateinit var browserAnimator: BrowserAnimator @@ -103,12 +102,9 @@ class DefaultBrowserToolbarControllerTest { every { activity.components.useCases.sessionUseCases } returns sessionUseCases every { activity.components.useCases.searchUseCases } returns searchUseCases every { activity.components.useCases.topSitesUseCase } returns topSitesUseCase - every { sessionManager.selectedSession } returns currentSession every { navController.currentDestination } returns mockk { every { id } returns R.id.browserFragment } - every { currentSession.id } returns "1" - every { currentSession.private } returns false val onComplete = slot<() -> Unit>() every { browserAnimator.captureEngineViewAndDrawStatically(capture(onComplete)) } answers { onComplete.captured.invoke() } @@ -248,14 +244,6 @@ class DefaultBrowserToolbarControllerTest { @Test fun handleToolbarCloseTabPressWithLastPrivateSession() { val item = TabCounterMenu.Item.CloseTab - val sessions = listOf( - mockk { - every { private } returns true - } - ) - - every { currentSession.private } returns true - every { sessionManager.sessions } returns sessions val controller = createController() controller.handleTabCounterItemInteraction(item) @@ -269,11 +257,13 @@ class DefaultBrowserToolbarControllerTest { fun handleToolbarCloseTabPress() { val item = TabCounterMenu.Item.CloseTab - every { sessionManager.sessions } returns emptyList() + val testTab = createTab("https://www.firefox.com") + store.dispatch(TabListAction.AddTabAction(testTab)).joinBlocking() + store.dispatch(TabListAction.SelectTabAction(testTab.id)).joinBlocking() val controller = createController() controller.handleTabCounterItemInteraction(item) - verify { sessionManager.remove(currentSession, selectParentIfExists = true) } + verify { tabsUseCases.removeTab(testTab.id, selectParentIfExists = true) } } @Test @@ -324,17 +314,17 @@ class DefaultBrowserToolbarControllerTest { private fun createController( activity: HomeActivity = this.activity, - customTabSession: Session? = null + customTabSessionId: String? = null ) = DefaultBrowserToolbarController( store = store, + tabsUseCases = tabsUseCases, activity = activity, navController = navController, metrics = metrics, engineView = engineView, homeViewModel = homeViewModel, - customTabSession = customTabSession, + customTabSessionId = customTabSessionId, readerModeController = readerModeController, - sessionManager = sessionManager, onTabCounterClicked = onTabCounterClicked, onCloseTab = onCloseTab ) diff --git a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt index 4082077ae..da473a25d 100644 --- a/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/components/toolbar/DefaultBrowserToolbarMenuControllerTest.kt @@ -24,10 +24,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest import mozilla.appservices.places.BookmarkRoot -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.SessionManager +import mozilla.components.browser.state.action.CustomTabListAction import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ReaderState +import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.concept.engine.EngineSession @@ -36,15 +37,18 @@ import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.tab.collections.TabCollection +import mozilla.components.feature.tabs.CustomTabsUseCases import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.support.base.feature.ViewBoundFeatureWrapper +import mozilla.components.support.test.ext.joinBlocking import mozilla.components.support.test.rule.MainCoroutineRule import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.R @@ -64,6 +68,7 @@ import org.mozilla.fenix.utils.Settings @OptIn(ExperimentalCoroutinesApi::class) @RunWith(FenixRobolectricTestRunner::class) +@Suppress("ForbiddenComment") class DefaultBrowserToolbarMenuControllerTest { @get:Rule @@ -74,13 +79,12 @@ class DefaultBrowserToolbarMenuControllerTest { @RelaxedMockK private lateinit var navController: NavController @RelaxedMockK private lateinit var findInPageLauncher: () -> Unit @RelaxedMockK private lateinit var bookmarkTapped: (String, String) -> Unit - @RelaxedMockK private lateinit var sessionManager: SessionManager - @RelaxedMockK private lateinit var currentSession: Session @RelaxedMockK private lateinit var openInFenixIntent: Intent @RelaxedMockK private lateinit var metrics: MetricController @RelaxedMockK private lateinit var settings: Settings @RelaxedMockK private lateinit var searchUseCases: SearchUseCases @RelaxedMockK private lateinit var sessionUseCases: SessionUseCases + @RelaxedMockK private lateinit var customTabUseCases: CustomTabsUseCases @RelaxedMockK private lateinit var browserAnimator: BrowserAnimator @RelaxedMockK private lateinit var snackbar: FenixSnackbar @RelaxedMockK private lateinit var tabCollectionStorage: TabCollectionStorage @@ -89,7 +93,9 @@ class DefaultBrowserToolbarMenuControllerTest { @MockK private lateinit var sessionFeatureWrapper: ViewBoundFeatureWrapper @RelaxedMockK private lateinit var sessionFeature: SessionFeature @RelaxedMockK private lateinit var topSitesStorage: DefaultTopSitesStorage - @RelaxedMockK private lateinit var browserStore: BrowserStore + + private lateinit var browserStore: BrowserStore + private lateinit var selectedTab: TabSessionState @Before fun setUp() { @@ -104,18 +110,25 @@ class DefaultBrowserToolbarMenuControllerTest { every { FenixSnackbar.make(any(), any(), any(), any()) } returns snackbar every { activity.components.useCases.sessionUseCases } returns sessionUseCases + every { activity.components.useCases.customTabsUseCases } returns customTabUseCases every { activity.components.useCases.searchUseCases } returns searchUseCases every { activity.components.useCases.topSitesUseCase } returns topSitesUseCase - every { sessionManager.selectedSession } returns currentSession every { sessionFeatureWrapper.get() } returns sessionFeature every { navController.currentDestination } returns mockk { every { id } returns R.id.browserFragment } - every { currentSession.id } returns "1" every { settings.topSitesMaxLimit } returns 16 val onComplete = slot<() -> Unit>() every { browserAnimator.captureEngineViewAndDrawStatically(capture(onComplete)) } answers { onComplete.captured.invoke() } + + selectedTab = createTab("https://www.mozilla.org", id = "1") + browserStore = BrowserStore( + initialState = BrowserState( + tabs = listOf(selectedTab), + selectedTabId = selectedTab.id + ) + ) } @After @@ -124,22 +137,133 @@ class DefaultBrowserToolbarMenuControllerTest { unmockkObject(FenixSnackbar.Companion) } + // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 + // todo === Start === + @Test + fun handleToolbarBookmarkPressWithReaderModeInactive() = runBlockingTest { + if (!FeatureFlags.toolbarMenuFeature) { + val item = ToolbarMenu.Item.Bookmark + + val title = "Mozilla" + val url = "https://mozilla.org" + val regularTab = createTab( + url = url, + readerState = ReaderState(active = false, activeUrl = "https://1234.org"), + title = title + ) + val store = + BrowserStore(BrowserState(tabs = listOf(regularTab), selectedTabId = regularTab.id)) + + val controller = createController(scope = this, store = store) + controller.handleToolbarItemInteraction(item) + + verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARK)) } + verify { bookmarkTapped(url, title) } + } + } + + @Test + fun `IF reader mode is active WHEN bookmark menu item is pressed THEN menu item is handled`() = runBlockingTest { + if (!FeatureFlags.toolbarMenuFeature) { + val item = ToolbarMenu.Item.Bookmark + val title = "Mozilla" + val readerUrl = "moz-extension://1234" + val readerTab = createTab( + url = readerUrl, + readerState = ReaderState(active = true, activeUrl = "https://mozilla.org"), + title = title + ) + browserStore = + BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id)) + + val controller = createController(scope = this, store = browserStore) + controller.handleToolbarItemInteraction(item) + + verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARK)) } + verify { bookmarkTapped("https://mozilla.org", title) } + } + } + + @Test + fun `WHEN open in Fenix menu item is pressed THEN menu item is handled correctly`() = runBlockingTest { + if (!FeatureFlags.toolbarMenuFeature) { + + val customTab = createCustomTab("https://mozilla.org") + browserStore.dispatch(CustomTabListAction.AddCustomTabAction(customTab)).joinBlocking() + val controller = createController( + scope = this, + store = browserStore, + customTabSessionId = customTab.id + ) + + val item = ToolbarMenu.Item.OpenInFenix + + every { activity.startActivity(any()) } just Runs + controller.handleToolbarItemInteraction(item) + + verify { sessionFeature.release() } + verify { customTabUseCases.migrate(customTab.id, true) } + verify { activity.startActivity(openInFenixIntent) } + verify { activity.finishAndRemoveTask() } + } + } + + @Test + fun `WHEN quit menu item is pressed THEN menu item is handled correctly`() = runBlockingTest { + if (!FeatureFlags.toolbarMenuFeature) { + val item = ToolbarMenu.Item.Quit + val testScope = this + + val controller = createController(scope = this, store = browserStore) + + controller.handleToolbarItemInteraction(item) + + verify { deleteAndQuit(activity, testScope, null) } + } + } + + @Test + fun handleToolbarOpenInAppPress() = runBlockingTest { + if (!FeatureFlags.toolbarMenuFeature) { + val item = ToolbarMenu.Item.OpenInApp + + val controller = createController(scope = this, store = browserStore) + + controller.handleToolbarItemInteraction(item) + + verify { settings.openInAppOpened = true } + } + } + + @Test + fun `WHEN reader mode menu item is pressed THEN handle appearance change`() = runBlockingTest { + val item = ToolbarMenu.Item.ReaderModeAppearance + + val controller = createController(scope = this, store = browserStore) + + controller.handleToolbarItemInteraction(item) + + verify { readerModeController.showControls() } + verify { metrics.track(Event.ReaderModeAppearanceOpened) } + } + // todo === End === + @Test - fun handleToolbarBackPress() = runBlockingTest { + fun `WHEN backwards nav menu item is pressed THEN the session navigates back with active session`() = runBlockingTest { val item = ToolbarMenu.Item.Back(false) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BACK)) } - verify { sessionUseCases.goBack(currentSession) } + verify { sessionUseCases.goBack(browserStore.state.selectedTabId!!) } } @Test - fun handleToolbarBackLongPress() = runBlockingTest { + fun `WHEN backwards nav menu item is long pressed THEN the session navigates back with no active session`() = runBlockingTest { val item = ToolbarMenu.Item.Back(true) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(null) @@ -149,21 +273,21 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarForwardPress() = runBlockingTest { + fun `WHEN forward nav menu item is pressed THEN the session navigates forward to active session`() = runBlockingTest { val item = ToolbarMenu.Item.Forward(false) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.FORWARD)) } - verify { sessionUseCases.goForward(currentSession) } + verify { sessionUseCases.goForward(selectedTab.id) } } @Test - fun handleToolbarForwardLongPress() = runBlockingTest { + fun `WHEN forward nav menu item is long pressed THEN the browser navigates forward with no active session`() = runBlockingTest { val item = ToolbarMenu.Item.Forward(true) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) val directions = BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(null) @@ -173,48 +297,48 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarReloadPress() = runBlockingTest { + fun `WHEN reload nav menu item is pressed THEN the session reloads from cache`() = runBlockingTest { val item = ToolbarMenu.Item.Reload(false) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.RELOAD)) } - verify { sessionUseCases.reload(currentSession) } + verify { sessionUseCases.reload(selectedTab.id) } } @Test - fun handleToolbarReloadLongPress() = runBlockingTest { + fun `WHEN reload nav menu item is long pressed THEN the session reloads with no cache`() = runBlockingTest { val item = ToolbarMenu.Item.Reload(true) - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.RELOAD)) } verify { sessionUseCases.reload( - currentSession, + selectedTab.id, EngineSession.LoadUrlFlags.select(EngineSession.LoadUrlFlags.BYPASS_CACHE) ) } } @Test - fun handleToolbarStopPress() = runBlockingTest { + fun `WHEN stop nav menu item is pressed THEN the session stops loading`() = runBlockingTest { val item = ToolbarMenu.Item.Stop - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.STOP)) } - verify { sessionUseCases.stopLoading(currentSession) } + verify { sessionUseCases.stopLoading(selectedTab.id) } } @Test - fun handleToolbarSettingsPress() = runBlockingTest { + fun `WHEN settings menu item is pressed THEN menu item is handled`() = runBlockingTest { val item = ToolbarMenu.Item.Settings - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() @@ -224,55 +348,10 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarBookmarkPressWithReaderModeInactive() = runBlockingTest { - val item = ToolbarMenu.Item.Bookmark - val title = "Mozilla" - val readerUrl = "moz-extension://1234" - val readerTab = createTab( - url = readerUrl, - readerState = ReaderState(active = false, activeUrl = "https://1234.org"), - title = title - ) - browserStore = - BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id)) - every { currentSession.id } returns readerTab.id - every { currentSession.title } returns title - every { currentSession.url } returns "https://mozilla.org" - - val controller = createController(scope = this) - controller.handleToolbarItemInteraction(item) - - verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARK)) } - verify { bookmarkTapped("https://mozilla.org", title) } - } - - @Test - fun handleToolbarBookmarkPressWithReaderModeActive() = runBlockingTest { - val item = ToolbarMenu.Item.Bookmark - val title = "Mozilla" - val readerUrl = "moz-extension://1234" - val readerTab = createTab( - url = readerUrl, - readerState = ReaderState(active = true, activeUrl = "https://mozilla.org"), - title = title - ) - browserStore = BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id)) - every { currentSession.id } returns readerTab.id - every { currentSession.title } returns title - every { currentSession.url } returns readerUrl - - val controller = createController(scope = this) - controller.handleToolbarItemInteraction(item) - - verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.BOOKMARK)) } - verify { bookmarkTapped("https://mozilla.org", title) } - } - - @Test - fun handleToolbarBookmarksPress() = runBlockingTest { + fun `WHEN bookmark menu item is pressed THEN navigate to bookmarks page`() = runBlockingTest { val item = ToolbarMenu.Item.Bookmarks - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) val directions = BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) @@ -282,10 +361,10 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarHistoryPress() = runBlockingTest { + fun `WHEN history menu item is pressed THEN navigate to history page`() = runBlockingTest { val item = ToolbarMenu.Item.History - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) val directions = BrowserFragmentDirections.actionGlobalHistoryFragment() @@ -295,47 +374,47 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarRequestDesktopOnPress() = runBlockingTest { + fun `WHEN request desktop menu item is toggled On THEN desktop site is requested for the session`() = runBlockingTest { val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase = mockk(relaxed = true) val item = ToolbarMenu.Item.RequestDesktop(true) every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON)) } verify { requestDesktopSiteUseCase.invoke( true, - currentSession + selectedTab.id ) } } @Test - fun handleToolbarRequestDesktopOffPress() = runBlockingTest { + fun `WHEN request desktop menu item is toggled Off THEN mobile site is requested for the session`() = runBlockingTest { val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase = mockk(relaxed = true) val item = ToolbarMenu.Item.RequestDesktop(false) every { sessionUseCases.requestDesktopSite } returns requestDesktopSiteUseCase - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF)) } verify { requestDesktopSiteUseCase.invoke( false, - currentSession + selectedTab.id ) } } @Test - fun handleToolbarAddToTopSitesPressed() = runBlockingTest { + fun `WHEN Add To Top Sites menu item is pressed THEN add site AND show snackbar`() = runBlockingTest { val item = ToolbarMenu.Item.AddToTopSites val addPinnedSiteUseCase: TopSitesUseCases.AddPinnedSiteUseCase = mockk(relaxed = true) @@ -344,50 +423,46 @@ class DefaultBrowserToolbarMenuControllerTest { swipeRefreshLayout.context.getString(R.string.snackbar_added_to_top_sites) } returns "Added to top sites!" - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) - verify { addPinnedSiteUseCase.invoke(currentSession.title, currentSession.url) } + verify { addPinnedSiteUseCase.invoke(selectedTab.content.title, selectedTab.content.url) } verify { snackbar.setText("Added to top sites!") } verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES)) } } @Test - fun handleToolbarAddonsManagerPress() = runBlockingTest { + fun `WHEN addon extensions menu item is pressed THEN navigate to addons manager`() = runBlockingTest { val item = ToolbarMenu.Item.AddonsManager - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER)) } } @Test - fun handleToolbarAddToHomeScreenPress() = runBlockingTest { + fun `WHEN Add To Home Screen menu item is pressed THEN add site`() = runBlockingTest { val item = ToolbarMenu.Item.AddToHomeScreen - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN)) } } @Test - fun handleToolbarSharePressWithReaderModeInactive() = runBlockingTest { + fun `IF reader mode is inactive WHEN share menu item is pressed THEN navigate to share screen`() = runBlockingTest { val item = ToolbarMenu.Item.Share val title = "Mozilla" - val readerUrl = "moz-extension://1234" - val readerTab = createTab( - url = readerUrl, + val url = "https://mozilla.org" + val regularTab = createTab( + url = url, readerState = ReaderState(active = false, activeUrl = "https://1234.org"), title = title ) - browserStore = BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id)) - every { currentSession.id } returns readerTab.id - every { currentSession.title } returns title - every { currentSession.url } returns "https://mozilla.org" - - val controller = createController(scope = this) + browserStore = BrowserStore(BrowserState(tabs = listOf(regularTab), selectedTabId = regularTab.id)) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SHARE)) } @@ -404,7 +479,7 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarSharePressWithReaderModeActive() = runBlockingTest { + fun `IF reader mode is active WHEN share menu item is pressed THEN navigate to share screen`() = runBlockingTest { val item = ToolbarMenu.Item.Share val title = "Mozilla" val readerUrl = "moz-extension://1234" @@ -414,11 +489,7 @@ class DefaultBrowserToolbarMenuControllerTest { title = title ) browserStore = BrowserStore(BrowserState(tabs = listOf(readerTab), selectedTabId = readerTab.id)) - every { currentSession.id } returns readerTab.id - every { currentSession.title } returns title - every { currentSession.url } returns readerUrl - - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SHARE)) } @@ -435,10 +506,10 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarFindInPagePress() = runBlockingTest { + fun `WHEN Find In Page menu item is pressed THEN launch finder`() = runBlockingTest { val item = ToolbarMenu.Item.FindInPage - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { findInPageLauncher() } @@ -446,12 +517,12 @@ class DefaultBrowserToolbarMenuControllerTest { } @Test - fun handleToolbarSaveToCollectionPressWhenAtLeastOneCollectionExists() = runBlockingTest { + fun `IF one or more collection exists WHEN Save To Collection menu item is pressed THEN navigate to save collection page`() = runBlockingTest { val item = ToolbarMenu.Item.SaveToCollection val cachedTabCollections: List = mockk(relaxed = true) every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { @@ -467,19 +538,19 @@ class DefaultBrowserToolbarMenuControllerTest { val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment( saveCollectionStep = SaveCollectionStep.SelectCollection, - tabIds = arrayOf(currentSession.id), - selectedTabIds = arrayOf(currentSession.id) + tabIds = arrayOf(selectedTab.id), + selectedTabIds = arrayOf(selectedTab.id) ) verify { navController.navigate(directionsEq(directions), null) } } @Test - fun handleToolbarSaveToCollectionPressWhenNoCollectionsExists() = runBlockingTest { + fun `IF no collection exists WHEN Save To Collection menu item is pressed THEN navigate to create collection page`() = runBlockingTest { val item = ToolbarMenu.Item.SaveToCollection val cachedTabCollectionsEmpty: List = emptyList() every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollectionsEmpty - val controller = createController(scope = this) + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) verify { metrics.track(Event.BrowserMenuItemTapped(Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION)) } @@ -492,84 +563,51 @@ class DefaultBrowserToolbarMenuControllerTest { } val directions = BrowserFragmentDirections.actionGlobalCollectionCreationFragment( saveCollectionStep = SaveCollectionStep.NameCollection, - tabIds = arrayOf(currentSession.id), - selectedTabIds = arrayOf(currentSession.id) + tabIds = arrayOf(selectedTab.id), + selectedTabIds = arrayOf(selectedTab.id) ) verify { navController.navigate(directionsEq(directions), null) } } @Test - fun handleToolbarOpenInFenixPress() = runBlockingTest { - val controller = createController(scope = this, customTabSession = currentSession) - - val item = ToolbarMenu.Item.OpenInFenix + fun `WHEN New Tab menu item is pressed THEN navigate to a new tab home`() = runBlockingTest { + val item = ToolbarMenu.Item.NewTab - every { currentSession.customTabConfig } returns mockk() - every { activity.startActivity(any()) } just Runs + val controller = createController(scope = this, store = browserStore) controller.handleToolbarItemInteraction(item) - verify { sessionFeature.release() } - verify { currentSession.customTabConfig = null } - verify { sessionManager.select(currentSession) } - verify { activity.startActivity(openInFenixIntent) } - verify { activity.finishAndRemoveTask() } - } - - @Test - fun handleToolbarQuitPress() = runBlockingTest { - val item = ToolbarMenu.Item.Quit - val testScope = this - - val controller = createController(scope = testScope) - - controller.handleToolbarItemInteraction(item) - - verify { deleteAndQuit(activity, testScope, null) } - } - - @Test - fun handleToolbarReaderModeAppearancePress() = runBlockingTest { - val item = ToolbarMenu.Item.ReaderModeAppearance - - val controller = createController(scope = this) - - controller.handleToolbarItemInteraction(item) - - verify { readerModeController.showControls() } - verify { metrics.track(Event.ReaderModeAppearanceOpened) } - } - - @Test - fun handleToolbarOpenInAppPress() = runBlockingTest { - val item = ToolbarMenu.Item.OpenInApp - - val controller = createController(scope = this) - - controller.handleToolbarItemInteraction(item) - - verify { settings.openInAppOpened = true } + verify { + navController.navigate( + directionsEq( + NavGraphDirections.actionGlobalHome( + focusOnAddressBar = true + ) + ) + ) + } } private fun createController( scope: CoroutineScope, + store: BrowserStore, activity: HomeActivity = this.activity, - customTabSession: Session? = null + customTabSessionId: String? = null ) = DefaultBrowserToolbarMenuController( + store = store, activity = activity, navController = navController, metrics = metrics, settings = settings, findInPageLauncher = findInPageLauncher, browserAnimator = browserAnimator, - customTabSession = customTabSession, + customTabSessionId = customTabSessionId, openInFenixIntent = openInFenixIntent, scope = scope, swipeRefresh = swipeRefreshLayout, tabCollectionStorage = tabCollectionStorage, bookmarkTapped = bookmarkTapped, readerModeController = readerModeController, - sessionManager = sessionManager, sessionFeature = sessionFeatureWrapper, topSitesStorage = topSitesStorage, browserStore = browserStore diff --git a/app/src/test/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehaviorTest.kt b/app/src/test/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehaviorTest.kt index 526125006..6b55b53d5 100644 --- a/app/src/test/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehaviorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/downloads/DynamicDownloadDialogBehaviorTest.kt @@ -60,7 +60,7 @@ class DynamicDownloadDialogBehaviorTest { } @Test - fun `Behavior will snap the dialog up if it is more than 50% visible`() { + fun `Behavior will snap the dialog up if it is more than 50 percent visible`() { val behavior = spyk(DynamicDownloadDialogBehavior(testContext, attrs = null, bottomToolbarHeight = 10f)) every { behavior.shouldScroll } returns true diff --git a/app/src/test/java/org/mozilla/fenix/ext/BookmarkNodeTest.kt b/app/src/test/java/org/mozilla/fenix/ext/BookmarkNodeTest.kt index 5f291061a..0f15ea076 100644 --- a/app/src/test/java/org/mozilla/fenix/ext/BookmarkNodeTest.kt +++ b/app/src/test/java/org/mozilla/fenix/ext/BookmarkNodeTest.kt @@ -5,22 +5,21 @@ package org.mozilla.fenix.ext import mozilla.components.concept.storage.BookmarkNode -import mozilla.components.concept.storage.BookmarkNodeType import org.junit.Assert.assertEquals import org.junit.Test +import org.mozilla.fenix.library.bookmarks.testBookmarkItem +import org.mozilla.fenix.library.bookmarks.testFolder class BookmarkNodeTest { - private val bookmarkChild1 = newBookmarkNode("Child 1", 1, null) - private val bookmarkChild2 = newBookmarkNode("Child 2", 2, null) - private val bookmarkChild3 = newBookmarkNode("Child 3", 3, null) + private val bookmarkChild1 = testBookmarkItem("someFolder", "http://www.mockurl.com/1", "Child 1") + private val bookmarkChild2 = testBookmarkItem("someFolder", "http://www.mockurl.com/2", "Child 2") + private val bookmarkChild3 = testBookmarkItem("someFolder", "http://www.mockurl.com/3", "Child 3") private val allChildren = listOf(bookmarkChild1, bookmarkChild2) - private var uniqueId = 0 - @Test fun `GIVEN a bookmark node with children WHEN minusing a sub set of children THEN the children subset is removed and rest remains`() { - val bookmarkNode = newBookmarkNode("Parent 1", 0, allChildren) + val bookmarkNode = testFolder("parent1", "root", allChildren) val subsetToSubtract = setOf(bookmarkChild1) val expectedRemainingSubset = listOf(bookmarkChild2) val bookmarkNodeSubsetRemoved = bookmarkNode.minus(subsetToSubtract) @@ -29,7 +28,7 @@ class BookmarkNodeTest { @Test fun `GIVEN a bookmark node with children WHEN minusing a set of all children THEN all children are removed and empty list remains`() { - val bookmarkNode = newBookmarkNode("Parent 1", 0, allChildren) + val bookmarkNode = testFolder("parent1", "root", allChildren) val setofAllChildren = setOf(bookmarkChild1, bookmarkChild2) val bookmarkNodeAllChildrenRemoved = bookmarkNode.minus(setofAllChildren) assertEquals(emptyList(), bookmarkNodeAllChildrenRemoved.children) @@ -38,21 +37,21 @@ class BookmarkNodeTest { @Test fun `GIVEN a bookmark node with children WHEN minusing a set of non-children THEN no children are removed`() { val setofNonChildren = setOf(bookmarkChild3) - val bookmarkNode = newBookmarkNode("Parent 1", 0, allChildren) + val bookmarkNode = testFolder("parent1", "root", allChildren) val bookmarkNodeNonChildrenRemoved = bookmarkNode.minus(setofNonChildren) assertEquals(allChildren, bookmarkNodeNonChildrenRemoved.children) } @Test fun `GIVEN a bookmark node with children WHEN minusing an empty set THEN no children are removed`() { - val bookmarkNode = newBookmarkNode("Parent 1", 0, allChildren) + val bookmarkNode = testFolder("parent1", "root", allChildren) val bookmarkNodeEmptySetRemoved = bookmarkNode.minus(emptySet()) assertEquals(allChildren, bookmarkNodeEmptySetRemoved.children) } @Test fun `GIVEN a bookmark node with an empty list as children WHEN minusing a set of non-children from an empty parent THEN an empty list remains`() { - val parentWithEmptyList = newBookmarkNode("Parent 1", 0, emptyList()) + val parentWithEmptyList = testFolder("parent1", "root", emptyList()) val setofAllChildren = setOf(bookmarkChild1, bookmarkChild2) val parentWithEmptyListNonChildRemoved = parentWithEmptyList.minus(setofAllChildren) assertEquals(emptyList(), parentWithEmptyListNonChildRemoved.children) @@ -60,14 +59,14 @@ class BookmarkNodeTest { @Test fun `GIVEN a bookmark node with null as children WHEN minusing a set of non-children from a parent with null children THEN null remains`() { - val parentWithNullList = newBookmarkNode("Parent 1", 0, null) + val parentWithNullList = testFolder("parent1", "root", null) val parentWithNullListNonChildRemoved = parentWithNullList.minus(allChildren.toSet()) assertEquals(null, parentWithNullListNonChildRemoved.children) } @Test fun `GIVEN a bookmark node with children WHEN minusing a sub-set of children THEN the rest of the parents object should remain the same`() { - val bookmarkNode = newBookmarkNode("Parent 1", 0, allChildren) + val bookmarkNode = testFolder("parent1", "root", allChildren) val subsetToSubtract = setOf(bookmarkChild1) val expectedRemainingSubset = listOf(bookmarkChild2) val resultBookmarkNode = bookmarkNode.minus(subsetToSubtract) @@ -77,14 +76,4 @@ class BookmarkNodeTest { val restofOriginal = bookmarkNode.copy(children = expectedRemainingSubset) assertEquals(restOfResult, restofOriginal) } - - private fun newBookmarkNode(title: String, position: Int, children: List?) = BookmarkNode( - type = BookmarkNodeType.ITEM, - guid = uniqueId++.toString(), - parentGuid = "12", - position = position, - title = title, - url = "www.mockurl.com", - children = children - ) } diff --git a/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt b/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt deleted file mode 100644 index 7c2c0a1d1..000000000 --- a/app/src/test/java/org/mozilla/fenix/ext/BrowserStateTest.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -package org.mozilla.fenix.ext - -import mozilla.components.browser.state.state.BrowserState -import mozilla.components.browser.state.state.MediaState -import mozilla.components.browser.state.state.createTab -import org.junit.Assert.assertEquals -import org.junit.Test - -private const val SESSION_ID_MOZILLA = "0" -private const val SESSION_ID_BCC = "1" -private const val SESSION_ID_BAD = "not a real session id" - -class BrowserStateTest { - - private val sessionMozilla = createTab(url = "www.mozilla.org", id = SESSION_ID_MOZILLA) - private val sessionBcc = createTab(url = "www.bcc.co.uk", id = SESSION_ID_BCC) - - @Test - fun `return media state if it matches tab id`() { - val state = BrowserState( - tabs = listOf(sessionBcc, sessionMozilla), - media = MediaState( - MediaState.Aggregate( - state = MediaState.State.PLAYING, - activeTabId = SESSION_ID_MOZILLA - )) - ) - - assertEquals(MediaState.State.PLAYING, state.getMediaStateForSession(SESSION_ID_MOZILLA)) - assertEquals(MediaState.State.NONE, state.getMediaStateForSession(SESSION_ID_BCC)) - assertEquals(MediaState.State.NONE, state.getMediaStateForSession(SESSION_ID_BAD)) - } -} diff --git a/app/src/test/java/org/mozilla/fenix/helpers/StackTraces.kt b/app/src/test/java/org/mozilla/fenix/helpers/StackTraces.kt new file mode 100644 index 000000000..19fc97f62 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/helpers/StackTraces.kt @@ -0,0 +1,39 @@ +/* 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.helpers + +/** + * A collection of test helper functions for manipulating stack traces. + */ +object StackTraces { + + /** + * Gets a stack trace from logcat output. To use this, you should remove the name of the + * Exception or "Caused by" lines causing the problem and only use the stack trace lines below + * it. See src/test/resources/EdmStorageProviderBaseLogcat.txt for an example. + */ + fun getStackTraceFromLogcat(logcatResourcePath: String): Array { + val logcat = javaClass.classLoader!!.getResource(logcatResourcePath).readText() + val lines = logcat.split('\n').filter(String::isNotBlank) + return lines.map(::logcatLineToStackTraceElement).toTypedArray() + } + + private fun logcatLineToStackTraceElement(line: String): StackTraceElement { + // Expected format: + // 02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1556) + + // Expected: android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk + val methodInfo = line.substringBefore('(').substringAfterLast(' ') + val methodName = methodInfo.substringAfterLast('.') + val declaringClass = methodInfo.substringBeforeLast('.') + + // Expected: StrictMode.java:1556 + val fileInfo = line.substringAfter('(').substringBefore(')') + val fileName = fileInfo.substringBefore(':') + val lineNumber = fileInfo.substringAfter(':').toInt() + + return StackTraceElement(declaringClass, methodName, fileName, lineNumber) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt index 6025ba8b3..794d34039 100644 --- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt @@ -468,7 +468,7 @@ class DefaultSessionControlControllerTest { ) } } finally { - unmockkStatic(SearchState::class) + unmockkStatic("mozilla.components.browser.state.state.SearchStateKt") } } diff --git a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt index 8f24fa8e8..b201fd2e5 100644 --- a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt @@ -8,7 +8,9 @@ import android.content.Context import io.mockk.every import io.mockk.mockk import io.mockk.spyk +import io.mockk.verify import kotlinx.coroutines.ExperimentalCoroutinesApi +import mozilla.components.browser.menu.view.MenuButton import org.junit.Assert import org.junit.Before import org.junit.Test @@ -65,4 +67,13 @@ class HomeFragmentTest { Assert.assertEquals(topSitesMaxLimit, topSitesConfig.totalSites) } + + @Test + fun `WHEN configuration changed menu is dismissed`() { + val menuButton: MenuButton = mockk(relaxed = true) + homeFragment.getMenuButton = { menuButton } + homeFragment.onConfigurationChanged(mockk(relaxed = true)) + + verify(exactly = 1) { menuButton.dismissMenu() } + } } diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt index ea155c2fc..045f5d680 100644 --- a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt @@ -14,7 +14,6 @@ import io.mockk.verifyOrder import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore -import mozilla.components.feature.media.service.AbstractMediaService import mozilla.components.feature.media.service.AbstractMediaSessionService import mozilla.components.feature.tabs.TabsUseCases import org.junit.Assert.assertFalse @@ -23,7 +22,6 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.BrowserDirection -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.ext.components import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @@ -68,9 +66,14 @@ class OpenSpecificTabIntentProcessorTest { @Test fun `GIVEN an intent with null extra string WHEN it is processed THEN openToBrowser should not be called`() { val intent = Intent().apply { - action = AbstractMediaService.Companion.ACTION_SWITCH_TAB + action = AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB } + val store = BrowserStore(BrowserState(tabs = listOf(createTab(id = TEST_SESSION_ID, url = "https:mozilla.org")))) + val tabUseCases: TabsUseCases = mockk(relaxed = true) + every { activity.components.core.store } returns store + every { activity.components.useCases.tabsUseCases } returns tabUseCases + assertFalse(processor.process(intent, navController, out)) verify(exactly = 0) { activity.openToBrowser(BrowserDirection.FromGlobal) } @@ -81,13 +84,8 @@ class OpenSpecificTabIntentProcessorTest { @Test fun `GIVEN an intent with correct action and extra string WHEN it is processed THEN session should be selected and openToBrowser should be called`() { val intent = Intent().apply { - if (newMediaSessionApi) { - action = AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB - putExtra(AbstractMediaSessionService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID) - } else { - action = AbstractMediaService.Companion.ACTION_SWITCH_TAB - putExtra(AbstractMediaService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID) - } + action = AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB + putExtra(AbstractMediaSessionService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID) } val store = BrowserStore(BrowserState(tabs = listOf(createTab(id = TEST_SESSION_ID, url = "https:mozilla.org")))) val tabUseCases: TabsUseCases = mockk(relaxed = true) diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt index b80e584a7..bc04b618b 100644 --- a/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/BookmarkAdapterTest.kt @@ -8,29 +8,20 @@ import io.mockk.mockk import io.mockk.spyk import io.mockk.verifyOrder import mozilla.components.concept.storage.BookmarkNode -import mozilla.components.concept.storage.BookmarkNodeType -import org.junit.Assert.assertFalse +import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue +import org.junit.Assert.assertFalse import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mozilla.fenix.helpers.FenixRobolectricTestRunner +import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkNodeViewHolder @RunWith(FenixRobolectricTestRunner::class) internal class BookmarkAdapterTest { private lateinit var bookmarkAdapter: BookmarkAdapter - private val item = BookmarkNode( - BookmarkNodeType.ITEM, - "456", - "123", - 0, - "Mozilla", - "http://mozilla.org", - null - ) - @Before fun setup() { bookmarkAdapter = spyk( @@ -40,33 +31,83 @@ internal class BookmarkAdapterTest { @Test fun `update adapter from tree of bookmark nodes, null tree returns empty list`() { - val tree = BookmarkNode( - BookmarkNodeType.FOLDER, "123", null, 0, "Mobile", null, listOf( - item, - BookmarkNode(BookmarkNodeType.SEPARATOR, "789", "123", 1, null, null, null), - BookmarkNode( - BookmarkNodeType.ITEM, - "987", - "123", - 2, - "Firefox", - "https://www.mozilla.org/en-US/firefox/", - null - ) + val tree = testFolder("123", "root", listOf( + testBookmarkItem("someFolder", "http://mozilla.org"), + testSeparator("123"), + testBookmarkItem("123", "https://www.mozilla.org/en-US/firefox/") ) ) bookmarkAdapter.updateData(tree, BookmarkFragmentState.Mode.Normal()) bookmarkAdapter.updateData(null, BookmarkFragmentState.Mode.Normal()) verifyOrder { bookmarkAdapter.updateData(tree, BookmarkFragmentState.Mode.Normal()) - bookmarkAdapter.notifyItemRangeInserted(0, 3) + bookmarkAdapter.notifyItemRangeInserted(0, 2) bookmarkAdapter.updateData(null, BookmarkFragmentState.Mode.Normal()) - bookmarkAdapter.notifyItemRangeRemoved(0, 3) + bookmarkAdapter.notifyItemRangeRemoved(0, 2) } } + @Test + fun `update adapter from tree of bookmark nodes, separators are excluded`() { + val sep1 = testSeparator("123") + val sep2 = testSeparator("123") + val item1 = testBookmarkItem("123", "http://mozilla.org") + val item2 = testBookmarkItem("123", "https://www.mozilla.org/en-US/firefox/") + val folder = testFolder("123", "root", title = "Mobile", children = listOf(item1, sep1, item2, sep2)) + bookmarkAdapter.updateData(folder, BookmarkFragmentState.Mode.Normal()) + verifyOrder { + bookmarkAdapter.updateData(folder, BookmarkFragmentState.Mode.Normal()) + bookmarkAdapter.notifyItemRangeInserted(0, 2) + } + + assertEquals(2, bookmarkAdapter.itemCount) + assertEquals(listOf(item1, item2), bookmarkAdapter.tree) + } + + @Test + fun `update adapter from tree of bookmark nodes, folders are moved to the top`() { + val sep1 = testSeparator("123") + val item1 = testBookmarkItem("123", "http://mozilla.org") + val item2 = testBookmarkItem("123", "https://www.mozilla.org/en-US/firefox/") + val item3 = testBookmarkItem("123", "https://www.mozilla.org/en-US/firefox/2") + val item4 = testBookmarkItem("125", "https://www.mozilla.org/en-US/firefox/3") + val folder2 = testFolder("124", "123", title = "Mobile 2", children = emptyList()) + val folder3 = testFolder("125", "123", title = "Mobile 3", children = listOf(item4)) + val folder4 = testFolder("126", "123", title = "Mobile 3", children = emptyList()) + val folder = testFolder("123", "root", title = "Mobile", children = listOf( + folder4, item1, sep1, item2, folder2, folder3, item3 + )) + bookmarkAdapter.updateData(folder, BookmarkFragmentState.Mode.Normal()) + verifyOrder { + bookmarkAdapter.updateData(folder, BookmarkFragmentState.Mode.Normal()) + bookmarkAdapter.notifyItemRangeInserted(0, 6) + } + + assertEquals(6, bookmarkAdapter.itemCount) + assertEquals(listOf(folder4, folder2, folder3, item1, item2, item3), bookmarkAdapter.tree) + } + + @Test + fun `get item view type for different types of nodes`() { + val sep1 = testSeparator("123") + val item1 = testBookmarkItem("123", "https://www.mozilla.org/en-US/firefox/") + val folder1 = testFolder("124", "123", title = "Mobile 2", children = emptyList()) + bookmarkAdapter.updateData( + testFolder("123", "root", listOf(sep1, item1, folder1)), + BookmarkFragmentState.Mode.Normal() + ) + + assertEquals(2, bookmarkAdapter.itemCount) + // item1 + assertEquals(BookmarkNodeViewHolder.LAYOUT_ID, bookmarkAdapter.getItemViewType(0)) + // folder1 + assertEquals(BookmarkNodeViewHolder.LAYOUT_ID, bookmarkAdapter.getItemViewType(1)) + // sep is dropped during update + } + @Test fun `items are the same if they have the same guids`() { + val item = testBookmarkItem("someFolder", "http://mozilla.org") assertTrue(createSingleItemDiffUtil(item, item).areItemsTheSame(0, 0)) assertTrue( createSingleItemDiffUtil( @@ -84,6 +125,7 @@ internal class BookmarkAdapterTest { @Test fun `equal items have same contents unless their selected state changes`() { + val item = testBookmarkItem("someFolder", "http://mozilla.org") assertTrue(createSingleItemDiffUtil(item, item).areContentsTheSame(0, 0)) assertFalse( createSingleItemDiffUtil(item, item.copy(position = 1)).areContentsTheSame(0, 0) diff --git a/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt b/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt index 42f185f32..3ea5539dc 100644 --- a/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt +++ b/app/src/test/java/org/mozilla/fenix/library/bookmarks/UtilsKtTest.kt @@ -16,26 +16,10 @@ import org.mozilla.fenix.helpers.FenixRobolectricTestRunner class UtilsKtTest { @Test fun `friendly root titles`() { - val url = BookmarkNode( - BookmarkNodeType.ITEM, - "456", - "folder", - 0, - "Mozilla", - "http://mozilla.org", - null - ) + val url = testBookmarkItem("folder", "http://mozilla.org", "Mozilla") assertEquals("Mozilla", friendlyRootTitle(testContext, url)) - val folder = BookmarkNode( - BookmarkNodeType.FOLDER, - "456", - "folder", - 0, - "Folder", - null, - null - ) + val folder = testFolder("456", "folder", null, "Folder") assertEquals("Folder", friendlyRootTitle(testContext, folder)) val root = folder.copy(guid = "root________", title = "root") @@ -65,67 +49,17 @@ class UtilsKtTest { @Test fun `flatNodeList various cases`() { - val url = BookmarkNode( - BookmarkNodeType.ITEM, - "456", - "folder", - 0, - "Mozilla", - "http://mozilla.org", - null - ) - val url2 = BookmarkNode( - BookmarkNodeType.ITEM, - "8674", - "folder2", - 0, - "Mozilla", - "http://mozilla.org", - null - ) + val url = testBookmarkItem("folder", "http://mozilla.org") + val url2 = testBookmarkItem("folder2", "http://mozilla.org") assertEquals(emptyList(), url.flatNodeList(null)) - val root = BookmarkNode( - BookmarkNodeType.FOLDER, - "root", - null, - 0, - "root", - null, - null - ) + val root = testFolder("root", null, null) assertEquals(listOf(BookmarkNodeWithDepth(0, root, null)), root.flatNodeList(null)) assertEquals(emptyList(), root.flatNodeList("root")) - val folder = BookmarkNode( - BookmarkNodeType.FOLDER, - "folder", - root.guid, - 0, - "folder", - null, - listOf(url) - ) - - val folder3 = BookmarkNode( - BookmarkNodeType.FOLDER, - "folder3", - "folder2", - 0, - "folder3", - null, - null - ) - - val folder2 = BookmarkNode( - BookmarkNodeType.FOLDER, - "folder2", - root.guid, - 0, - "folder2", - null, - listOf(folder3, url2) - ) + val folder = testFolder("folder", root.guid, listOf(url)) + val folder3 = testFolder("folder3", "folder2", null) + val folder2 = testFolder("folder2", root.guid, listOf(folder3, url2)) val rootWithChildren = root.copy(children = listOf(folder, folder2)) assertEquals( @@ -145,3 +79,27 @@ class UtilsKtTest { ) } } + +internal fun testBookmarkItem(parentGuid: String, url: String, title: String = "Item for $url") = BookmarkNode( + BookmarkNodeType.ITEM, + "guid#${Math.random() * 1000}", + parentGuid, + 0, + title, + url, + null +) + +internal fun testFolder(guid: String, parentGuid: String?, children: List?, title: String = "Folder: $guid") = BookmarkNode( + BookmarkNodeType.FOLDER, + guid, + parentGuid, + 0, + title, + null, + children +) + +internal fun testSeparator(parentGuid: String) = BookmarkNode( + BookmarkNodeType.SEPARATOR, "guid#${Math.random() * 1000}", parentGuid, null, null, null, null +) diff --git a/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt index 59698c783..dc09d1fba 100644 --- a/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt +++ b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt @@ -26,7 +26,8 @@ class PerformanceInflaterTest { private val layoutsNotToTest = setOf( "fragment_browser", - "fragment_add_on_internal_settings" + "fragment_add_on_internal_settings", + "activity_privacy_content_display" ) @Before diff --git a/app/src/test/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessorTest.kt new file mode 100644 index 000000000..d0763e75a --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/ProfilerMarkerFactProcessorTest.kt @@ -0,0 +1,89 @@ +/* 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.os.Handler +import android.os.Looper +import io.mockk.Called +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import mozilla.components.concept.base.profiler.Profiler +import mozilla.components.support.base.Component +import mozilla.components.support.base.facts.Action +import mozilla.components.support.base.facts.Fact +import org.junit.Before +import org.junit.Test + +class ProfilerMarkerFactProcessorTest { + + @RelaxedMockK lateinit var profiler: Profiler + @RelaxedMockK lateinit var mainHandler: Handler + lateinit var processor: ProfilerMarkerFactProcessor + + var myLooper: Looper? = null + + @Before + fun setUp() { + MockKAnnotations.init(this) + myLooper = null + processor = ProfilerMarkerFactProcessor({ profiler }, mainHandler, { myLooper }) + } + + @Test + fun `GIVEN we are on the main thread WHEN a fact with an implementation detail action is received THEN a profiler marker is added now`() { + myLooper = mainHandler.looper // main thread + + val fact = newFact(Action.IMPLEMENTATION_DETAIL) + processor.process(fact) + + verify { profiler.addMarker(fact.item) } + } + + @Test + fun `GIVEN we are not on the main thread WHEN a fact with an implementation detail action is received THEN adding the marker is posted to the main thread`() { + myLooper = mockk() // off main thread + val mainThreadPostedSlot = slot() + every { profiler.getProfilerTime() } returns 100.0 + + val fact = newFact(Action.IMPLEMENTATION_DETAIL) + processor.process(fact) + + verify { mainHandler.post(capture(mainThreadPostedSlot)) } + verifyProfilerAddMarkerWasNotCalled() + + mainThreadPostedSlot.captured.run() // call the captured function posted to the main thread. + verify { profiler.addMarker(fact.item, 100.0, 100.0, null) } + } + + @Test + fun `WHEN a fact with a non-implementation detail action is received THEN no profiler marker is added`() { + val fact = newFact(Action.CANCEL) + processor.process(fact) + verify { profiler wasNot Called } + } + + private fun verifyProfilerAddMarkerWasNotCalled() { + verify(exactly = 0) { + profiler.addMarker(any()) + profiler.addMarker(any(), any() as Double?) + profiler.addMarker(any(), any() as String?) + profiler.addMarker(any(), any(), any()) + profiler.addMarker(any(), any(), any(), any()) + } + } +} + +private fun newFact( + action: Action, + item: String = "itemName" +) = Fact( + Component.BROWSER_SESSION, + action, + item +) diff --git a/app/src/test/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurementTest.kt b/app/src/test/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurementTest.kt index 7fadf1f28..fa465b2fb 100644 --- a/app/src/test/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurementTest.kt +++ b/app/src/test/java/org/mozilla/fenix/perf/StartupFrameworkStartMeasurementTest.kt @@ -17,7 +17,8 @@ import org.junit.Before import org.junit.Test import org.mozilla.fenix.GleanMetrics.StartupTimeline as Telemetry -private const val CLOCK_TICKS_PER_SECOND = 100L +private const val PRIMARY_TICKS = 100L +private const val SECONDARY_TICKS = 50L class StartupFrameworkStartMeasurementTest { @@ -27,10 +28,13 @@ class StartupFrameworkStartMeasurementTest { // We'd prefer to use the Glean test methods over these mocks but they require us to add // Robolectric and it's not worth the impact on test duration. @MockK private lateinit var telemetry: Telemetry - @MockK(relaxed = true) private lateinit var frameworkStart: TimespanMetricType + @MockK(relaxed = true) private lateinit var frameworkPrimary: TimespanMetricType + @MockK(relaxed = true) private lateinit var frameworkSecondary: TimespanMetricType @MockK(relaxed = true) private lateinit var frameworkStartError: BooleanMetricType @MockK(relaxed = true) private lateinit var clockTicksPerSecond: CounterMetricType + private var clockTicksPerSecondValue = -1L + private var elapsedRealtimeNanos = -1L private var processStartTimeTicks = -1L @@ -41,13 +45,17 @@ class StartupFrameworkStartMeasurementTest { elapsedRealtimeNanos = -1 processStartTimeTicks = -1 + // This value is hard-coded in the OS so we default to it as it's the expected value. + clockTicksPerSecondValue = PRIMARY_TICKS + stat = spyk(object : Stat() { - override val clockTicksPerSecond: Long get() = CLOCK_TICKS_PER_SECOND + override val clockTicksPerSecond: Long get() = clockTicksPerSecondValue }) every { stat.getProcessStartTimeTicks(any()) } answers { processStartTimeTicks } val getElapsedRealtimeNanos = { elapsedRealtimeNanos } - every { telemetry.frameworkStart } returns frameworkStart + every { telemetry.frameworkPrimary } returns frameworkPrimary + every { telemetry.frameworkSecondary } returns frameworkSecondary every { telemetry.frameworkStartError } returns frameworkStartError every { telemetry.clockTicksPerSecond } returns clockTicksPerSecond @@ -67,13 +75,32 @@ class StartupFrameworkStartMeasurementTest { } @Test - fun `GIVEN app init is set to valid values WHEN metrics are set THEN frameworkStart is set with the correct value`() { + fun `GIVEN app init is set to valid values and clock ticks per second is 100 WHEN metrics are set THEN frameworkPrimary is set with the correct value`() { + clockTicksPerSecondValue = PRIMARY_TICKS + setProcessAppInitAndMetrics(processStart = 166_636_813, appInit = 1_845_312_345_673_925) + verifyFrameworkStartSuccess(178_944_220_000_000, isPrimary = true) // calculated by hand. + } + + @Test + fun `GIVEN app init is set to valid values and clock ticks per second is not 100 WHEN metrics are set THEN frameworkSecondary is set with the correct value`() { + clockTicksPerSecondValue = SECONDARY_TICKS setProcessAppInitAndMetrics(processStart = 166_636_813, appInit = 1_845_312_345_673_925) - verifyFrameworkStartSuccess(178_944_220_000_000) // calculated by hand. + verifyFrameworkStartSuccess(178_944_220_000_000, isPrimary = false) // calculated by hand. } @Test // this overlaps with the success case test. - fun `GIVEN app init has valid values WHEN onAppInit is called twice and metrics are set THEN frameworkStart uses the first app init value`() { + fun `GIVEN app init has valid values and clock ticks per second is 100 WHEN onAppInit is called twice and metrics are set THEN frameworkPrimary uses the first app init value`() { + clockTicksPerSecondValue = PRIMARY_TICKS + testAppInitCalledTwice(isPrimary = true) + } + + @Test // this overlaps with the success case test. + fun `GIVEN app init has valid values and clock ticks per second is not 100 WHEN onAppInit is called twice and metrics are set THEN frameworkSecondary uses the first app init value`() { + clockTicksPerSecondValue = SECONDARY_TICKS + testAppInitCalledTwice(isPrimary = false) + } + + private fun testAppInitCalledTwice(isPrimary: Boolean) { processStartTimeTicks = 166_636_813 elapsedRealtimeNanos = 1_845_312_345_673_925 @@ -82,14 +109,28 @@ class StartupFrameworkStartMeasurementTest { metrics.onApplicationInit() metrics.setExpensiveMetric() - verifyFrameworkStartSuccess(178_944_220_000_000) // calculated by hand. + verifyFrameworkStartSuccess(178_944_220_000_000, isPrimary) // calculated by hand. } @Test - fun `GIVEN app init have valid values WHEN metrics are set twice THEN frameworkStart is only set once`() { + fun `GIVEN app init have valid values and clock ticks per second is 100 WHEN metrics are set twice THEN frameworkPrimary is only set once`() { + clockTicksPerSecondValue = PRIMARY_TICKS + testMetricsSetTwice(isPrimary = true) + } + + @Test + fun `GIVEN app init have valid values and clock ticks per second is not 100 WHEN metrics are set twice THEN frameworkSecondary is only set once`() { + clockTicksPerSecondValue = SECONDARY_TICKS + testMetricsSetTwice(isPrimary = false) + } + + private fun testMetricsSetTwice(isPrimary: Boolean) { setProcessAppInitAndMetrics(10, 100) metrics.setExpensiveMetric() - verify(exactly = 1) { frameworkStart.setRawNanos(any()) } + + val (setMetric, unsetMetric) = getSetAndUnsetMetric(isPrimary) + verify(exactly = 1) { setMetric.setRawNanos(any()) } + verify { unsetMetric wasNot Called } verify(exactly = 1) { clockTicksPerSecond.add(any()) } verify { frameworkStartError wasNot Called } } @@ -103,15 +144,33 @@ class StartupFrameworkStartMeasurementTest { metrics.setExpensiveMetric() } - private fun verifyFrameworkStartSuccess(nanos: Long) { - verify { frameworkStart.setRawNanos(nanos) } - verify { clockTicksPerSecond.add(CLOCK_TICKS_PER_SECOND.toInt()) } + private fun verifyFrameworkStartSuccess(nanos: Long, isPrimary: Boolean) { + val (setMetric, unsetMetric) = getSetAndUnsetMetric(isPrimary) + verify { setMetric.setRawNanos(nanos) } + verify { unsetMetric wasNot Called } + + val expectedClockTicksPerSecond = getExpectedClockTicksPerSecond(isPrimary) + verify { clockTicksPerSecond.add(expectedClockTicksPerSecond.toInt()) } verify { frameworkStartError wasNot Called } } private fun verifyFrameworkStartError() { verify { frameworkStartError.set(true) } - verify { frameworkStart wasNot Called } + verify { frameworkPrimary wasNot Called } + verify { frameworkSecondary wasNot Called } verify { clockTicksPerSecond wasNot Called } } + + private fun getSetAndUnsetMetric(isPrimary: Boolean): Pair { + return if (isPrimary) { + Pair(frameworkPrimary, frameworkSecondary) + } else { + Pair(frameworkSecondary, frameworkPrimary) + } + } + + // This hard-codes some data that's passed into the test but I don't want to spend more time + // so I don't bother cleaning it up now. + private fun getExpectedClockTicksPerSecond(isPrimary: Boolean): Long = + if (isPrimary) PRIMARY_TICKS else SECONDARY_TICKS } diff --git a/app/src/test/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListenerTest.kt b/app/src/test/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListenerTest.kt new file mode 100644 index 000000000..f046e406c --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/perf/ThreadPenaltyDeathWithIgnoresListenerTest.kt @@ -0,0 +1,78 @@ +/* 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.os.strictmode.Violation +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.RelaxedMockK +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import mozilla.components.support.base.log.logger.Logger +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.mozilla.fenix.helpers.StackTraces +import org.mozilla.fenix.utils.ManufacturerCodes + +class ThreadPenaltyDeathWithIgnoresListenerTest { + + @RelaxedMockK private lateinit var logger: Logger + private lateinit var listener: ThreadPenaltyDeathWithIgnoresListener + @MockK private lateinit var violation: Violation + private lateinit var stackTrace: Array + + @Before + fun setUp() { + MockKAnnotations.init(this) + + listener = ThreadPenaltyDeathWithIgnoresListener(logger) + + stackTrace = emptyArray() + every { violation.stackTrace } answers { stackTrace } + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test(expected = RuntimeException::class) + fun `WHEN provided an arbitrary violation that does not conflict with ignores THEN we throw an exception`() { + stackTrace = Exception().stackTrace + listener.onThreadViolation(violation) + } + + @Test + fun `GIVEN we're on a Samsung WHEN provided the EdmStorageProvider violation THEN it will be ignored and logged`() { + mockkObject(ManufacturerCodes) + every { ManufacturerCodes.isSamsung } returns true + + every { violation.stackTrace } returns getEdmStorageProviderStackTrace() + listener.onThreadViolation(violation) + + verify { logger.debug("Ignoring StrictMode ThreadPolicy violation", violation) } + } + + @Test(expected = RuntimeException::class) + fun `GIVEN we're not on a Samsung or LG WHEN provided the EdmStorageProvider violation THEN we throw an exception`() { + mockkObject(ManufacturerCodes) + every { ManufacturerCodes.isSamsung } returns false + every { ManufacturerCodes.isLG } returns false + + every { violation.stackTrace } returns getEdmStorageProviderStackTrace() + listener.onThreadViolation(violation) + } + + @Test + fun `WHEN violation is null THEN we don't throw an exception`() { + listener.onThreadViolation(null) + } + + private fun getEdmStorageProviderStackTrace() = + StackTraces.getStackTraceFromLogcat("EdmStorageProviderBaseLogcat.txt") +} diff --git a/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt index ed0a7c3dc..39d4382c2 100644 --- a/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt @@ -362,12 +362,12 @@ class ShareControllerTest { } @Test - fun `getShareSubject will return "shareSubject" if that is non null`() { + fun `getShareSubject will return 'shareSubject' if that is non null`() { assertEquals(shareSubject, controller.getShareSubject()) } @Test - fun `getShareSubject will return a concatenation of tab titles if "shareSubject" is null`() { + fun `getShareSubject will return a concatenation of tab titles if 'shareSubject' is null`() { val controller = DefaultShareController( context, null, shareData, sendTabUseCases, snackbar, navController, recentAppStorage, testCoroutineScope, testDispatcher, dismiss diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt index 8e7f7599a..3c974a981 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt @@ -32,7 +32,6 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode @@ -179,15 +178,6 @@ class DefaultTabTrayControllerTest { } } - @Test - fun onSyncedTabClicked() { - controller.handleSyncedTabClicked(mockk(relaxed = true)) - - verify { - activity.openToBrowserAndLoad(any(), true, BrowserDirection.FromTabTray) - } - } - @Test fun handleBackPressed() { every { tabTrayFragmentStore.state.mode } returns TabTrayDialogFragmentState.Mode.MultiSelect( diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/SyncedTabsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/SyncedTabsControllerTest.kt deleted file mode 100644 index 01e9c598f..000000000 --- a/app/src/test/java/org/mozilla/fenix/tabtray/SyncedTabsControllerTest.kt +++ /dev/null @@ -1,140 +0,0 @@ -package org.mozilla.fenix.tabtray - -import android.view.LayoutInflater -import android.view.View -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LifecycleRegistry -import androidx.recyclerview.widget.ConcatAdapter -import io.mockk.Called -import io.mockk.every -import io.mockk.mockk -import io.mockk.verify -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestCoroutineDispatcher -import kotlinx.coroutines.test.runBlockingTest -import mozilla.components.browser.storage.sync.SyncedDeviceTabs -import mozilla.components.feature.syncedtabs.view.SyncedTabsView.ErrorType -import mozilla.components.support.test.ext.joinBlocking -import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.rule.MainCoroutineRule -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mozilla.fenix.R -import org.mozilla.fenix.components.metrics.MetricController -import org.mozilla.fenix.helpers.FenixRobolectricTestRunner -import org.mozilla.fenix.sync.SyncedTabsViewHolder -import org.mozilla.fenix.tabtray.TabTrayDialogFragmentAction.EnterMultiSelectMode -import org.mozilla.fenix.tabtray.TabTrayDialogFragmentAction.ExitMultiSelectMode -import org.mozilla.fenix.tabtray.TabTrayDialogFragmentState.Mode - -@ExperimentalCoroutinesApi -@RunWith(FenixRobolectricTestRunner::class) -class SyncedTabsControllerTest { - - private val testDispatcher = TestCoroutineDispatcher() - @get:Rule - val coroutinesTestRule = MainCoroutineRule(testDispatcher) - - private lateinit var view: View - private lateinit var controller: SyncedTabsController - private lateinit var lifecycleOwner: LifecycleOwner - private lateinit var lifecycle: LifecycleRegistry - private lateinit var concatAdapter: ConcatAdapter - private lateinit var store: TabTrayDialogFragmentStore - - @Before - fun setup() = runBlockingTest { - lifecycleOwner = mockk() - lifecycle = LifecycleRegistry(lifecycleOwner) - lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) - every { lifecycleOwner.lifecycle } returns lifecycle - - concatAdapter = mockk() - every { concatAdapter.addAdapter(any()) } returns true - every { concatAdapter.removeAdapter(any()) } returns true - - store = TabTrayDialogFragmentStore( - initialState = TabTrayDialogFragmentState( - mode = Mode.Normal, - browserState = mockk(relaxed = true) - ) - ) - - view = LayoutInflater.from(testContext).inflate(R.layout.about_list_item, null) - val metrics: MetricController = mockk() - controller = - SyncedTabsController(lifecycleOwner, view, store, concatAdapter, coroutineContext, metrics) - } - - @After - fun cleanUp() { - testDispatcher.cleanupTestCoroutines() - } - - @Test - fun `display synced tabs in reverse`() { - val tabs = listOf( - SyncedDeviceTabs( - device = mockk(relaxed = true), - tabs = listOf( - mockk(relaxed = true), - mockk(relaxed = true) - ) - ) - ) - - controller.displaySyncedTabs(tabs) - - val itemCount = controller.adapter.itemCount - - // title + device name + 2 tabs - assertEquals(4, itemCount) - assertEquals( - SyncedTabsViewHolder.TitleViewHolder.LAYOUT_ID, - controller.adapter.getItemViewType(itemCount - 1) - ) - assertEquals( - SyncedTabsViewHolder.DeviceViewHolder.LAYOUT_ID, - controller.adapter.getItemViewType(itemCount - 2) - ) - assertEquals( - SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, - controller.adapter.getItemViewType(itemCount - 3) - ) - assertEquals( - SyncedTabsViewHolder.TabViewHolder.LAYOUT_ID, - controller.adapter.getItemViewType(itemCount - 4) - ) - } - - @Test - fun `show error when we go kaput`() { - controller.onError(ErrorType.SYNC_NEEDS_REAUTHENTICATION) - - assertEquals(1, controller.adapter.itemCount) - assertEquals( - SyncedTabsViewHolder.ErrorViewHolder.LAYOUT_ID, - controller.adapter.getItemViewType(0) - ) - } - - @Test - fun `do nothing on init, drop first event`() { - verify { concatAdapter wasNot Called } - } - - @Test - fun `concatAdapter updated on mode changes`() = testDispatcher.runBlockingTest { - store.dispatch(EnterMultiSelectMode).joinBlocking() - verify { concatAdapter.removeAdapter(any()) } - - store.dispatch(ExitMultiSelectMode).joinBlocking() - // When returning from Multiselect the adapter should be added at the end - verify { concatAdapter.addAdapter(any()) } - } -} diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt index a0e50b017..5504c715a 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayFragmentInteractorTest.kt @@ -89,12 +89,6 @@ class TabTrayFragmentInteractorTest { verify { controller.handleCloseAllTabsClicked(true) } } - @Test - fun onSyncedTabClicked() { - interactor.onSyncedTabClicked(mockk(relaxed = true)) - verify { controller.handleSyncedTabClicked(any()) } - } - @Test fun onBackPressed() { interactor.onBackPressed() diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt index f55d4d6cc..144abb980 100644 --- a/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt +++ b/app/src/test/java/org/mozilla/fenix/tabtray/TabTrayViewHolderTest.kt @@ -17,7 +17,6 @@ import io.mockk.verify import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ContentState import mozilla.components.browser.state.state.MediaSessionState -import mozilla.components.browser.state.state.MediaState import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.store.BrowserStore @@ -31,7 +30,6 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mozilla.fenix.FeatureFlags.newMediaSessionApi import org.mozilla.fenix.R import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.helpers.FenixRobolectricTestRunner @@ -83,31 +81,20 @@ class TabTrayViewHolderTest { url = "https://example.com" ) - if (newMediaSessionApi) { - state = state.copy( - tabs = listOf( - TabSessionState( - id = "123", - content = ContentState( - url = "https://example.com", - searchTerms = "search terms" - ), - mediaSessionState = mediaSessionState - ) + state = state.copy( + tabs = listOf( + TabSessionState( + id = "123", + content = ContentState( + url = "https://example.com", + searchTerms = "search terms" + ), + mediaSessionState = mediaSessionState ) ) + ) - every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PAUSED } - } else { - state = state.copy( - media = MediaState( - aggregate = MediaState.Aggregate( - activeTabId = "123", - state = MediaState.State.PAUSED - ) - ) - ) - } + every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PAUSED } tabViewHolder.bind(tab, false, mockk(), mockk()) @@ -124,31 +111,20 @@ class TabTrayViewHolderTest { url = "https://example.com" ) - if (newMediaSessionApi) { - state = state.copy( - tabs = listOf( - TabSessionState( - id = "123", - content = ContentState( - url = "https://example.com", - searchTerms = "search terms" - ), - mediaSessionState = mediaSessionState - ) + state = state.copy( + tabs = listOf( + TabSessionState( + id = "123", + content = ContentState( + url = "https://example.com", + searchTerms = "search terms" + ), + mediaSessionState = mediaSessionState ) ) + ) - every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PLAYING } - } else { - state = state.copy( - media = MediaState( - aggregate = MediaState.Aggregate( - activeTabId = "123", - state = MediaState.State.PLAYING - ) - ) - ) - } + every { mediaSessionState.playbackState } answers { MediaSession.PlaybackState.PLAYING } tabViewHolder.bind(tab, false, mockk(), mockk()) diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt index 59f5753c7..98da76499 100644 --- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt +++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt @@ -156,7 +156,9 @@ class TrackingProtectionOverlayTest { fun `show onboarding when trackers are blocked`() { every { toolbar.hasWindowFocus() } returns true every { settings.shouldShowTrackingProtectionCfr } returns true + every { session.content.progress } returns 100 every { session.content.loading } returns false + every { settings.shouldUseTrackingProtection } returns true every { session.trackingProtection } returns TrackingProtectionState( enabled = true, blockedTrackers = listOf(mockk()) @@ -165,6 +167,20 @@ class TrackingProtectionOverlayTest { verify { settings.incrementTrackingProtectionOnboardingCount() } } + @Test + fun `no-op when trackers are blocked but not finished loading`() { + every { toolbar.hasWindowFocus() } returns true + every { settings.shouldShowTrackingProtectionCfr } returns true + every { session.content.progress } returns 50 + every { session.content.loading } returns false + every { session.trackingProtection } returns TrackingProtectionState( + enabled = true, + blockedTrackers = listOf(mockk()) + ) + overlay.onLoadingStateChanged(session) + verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() } + } + @Test fun `no-op when toolbar doesn't have focus`() { every { toolbar.hasWindowFocus() } returns false diff --git a/app/src/test/java/org/mozilla/fenix/utils/ManufacturerCodesTest.kt b/app/src/test/java/org/mozilla/fenix/utils/ManufacturerCodesTest.kt new file mode 100644 index 000000000..f277af239 --- /dev/null +++ b/app/src/test/java/org/mozilla/fenix/utils/ManufacturerCodesTest.kt @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.fenix.utils + +import android.os.Build +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import java.lang.reflect.Modifier + +class ManufacturerCodesTest { + + private val manufacturerField = Build::class.java.getDeclaredField("MANUFACTURER") + private var manufacturer: String? + get() = Build.MANUFACTURER + set(value) { manufacturerField.set(null, value) } + + @Before + fun setUp() { + enableManufacturerModifications() + manufacturer = null // reset to default state before test. + assertEquals(null, Build.MANUFACTURER) // sanity check. + } + + private fun enableManufacturerModifications() { + // Mocking, which might be simpler, doesn't seem to work so we use reflection. + // Methodology via https://stackoverflow.com/a/3301720/2219998 + val modifiers = manufacturerField.javaClass.getDeclaredField("modifiers") + modifiers.isAccessible = true + modifiers.setInt(manufacturerField, manufacturerField.modifiers and Modifier.FINAL.inv()) + } + + @After + fun tearDown() { + // After this method, Build.MANUFACTURER appears to return to + // static final so we don't need to undo that. + manufacturer = null + assertEquals(null, Build.MANUFACTURER) // sanity check. + } + + @Test // To reduce boilerplate, we avoid best practice and put several tests in one. + fun testIsLG() { + manufacturer = "LGE" // expected value for lg devices + assertTrue(ManufacturerCodes.isLG) + + manufacturer = "lge" // unexpected value but is still an lg device + assertTrue(ManufacturerCodes.isLG) + + manufacturer = "samsung" + assertFalse(ManufacturerCodes.isLG) + } + + @Test // To reduce boilerplate, we avoid best practice and put several tests in one. + fun testIsSamsung() { + manufacturer = "samsung" // expected value for samsung devices + assertTrue(ManufacturerCodes.isSamsung) + + manufacturer = "SaMsUnG" // unexpected value but is still a samsung device + assertTrue(ManufacturerCodes.isSamsung) + + manufacturer = "LGE" + assertFalse(ManufacturerCodes.isSamsung) + } +} diff --git a/app/src/test/java/org/mozilla/fenix/utils/ToolbarPopupWindowTest.kt b/app/src/test/java/org/mozilla/fenix/utils/ToolbarPopupWindowTest.kt index 12ec801cd..69ef82b3a 100644 --- a/app/src/test/java/org/mozilla/fenix/utils/ToolbarPopupWindowTest.kt +++ b/app/src/test/java/org/mozilla/fenix/utils/ToolbarPopupWindowTest.kt @@ -4,11 +4,10 @@ package org.mozilla.fenix.utils -import io.mockk.every import io.mockk.mockk -import mozilla.components.browser.session.Session import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.ReaderState +import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import org.junit.Assert.assertEquals @@ -21,8 +20,7 @@ class ToolbarPopupWindowTest { @Test fun getUrlForClipboard() { - val customTabSession: Session = mockk() - every { customTabSession.url } returns "https://mozilla.org" + val customTabSession = createCustomTab("https://mozilla.org") // Custom tab assertEquals( diff --git a/app/src/test/resources/EdmStorageProviderBaseLogcat.txt b/app/src/test/resources/EdmStorageProviderBaseLogcat.txt new file mode 100644 index 000000000..1eb534116 --- /dev/null +++ b/app/src/test/resources/EdmStorageProviderBaseLogcat.txt @@ -0,0 +1,20 @@ +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1556) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.applyBlockGuardPolicy(SQLiteConnection.java:1524) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteConnection.executeForCursorWindow(SQLiteConnection.java:1193) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteSession.executeForCursorWindow(SQLiteSession.java:838) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:62) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:165) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:152) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:232) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:271) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.enterprise.storage.EdmStorageProviderBase.getValues(EdmStorageProviderBase.java:388) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.enterprise.storage.EdmStorageProviderBase.getValuesList(EdmStorageProviderBase.java:1104) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.enterprise.storage.EdmStorageProvider.getValuesList(EdmStorageProvider.java:300) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.enterprise.application.ApplicationPolicy.getApplicationStateEnabledAsUser(ApplicationPolicy.java:3745) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.enterprise.application.ApplicationPolicy.getApplicationStateEnabledAsUser(ApplicationPolicy.java:3714) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.pm.PackageManagerService.setEnabledSetting(PackageManagerService.java:26031) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.pm.PackageManagerService.setComponentEnabledSetting(PackageManagerService.java:26007) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.content.pm.IPackageManager$Stub.onTransact(IPackageManager.java:3774) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at com.android.server.pm.PackageManagerService.onTransact(PackageManagerService.java:5145) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.os.Binder.execTransactInternal(Binder.java:1056) +02-08 10:56:02.185 21990 21990 E AndroidRuntime: at android.os.Binder.execTransact(Binder.java:1029) diff --git a/build.gradle b/build.gradle index e1399e459..093a4fcd2 100644 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,7 @@ buildscript { includeGroupByRegex RepoMatching.mozilla } } + maven { name "Mozilla" url "https://maven.mozilla.org/maven2" @@ -21,6 +22,7 @@ buildscript { includeGroupByRegex RepoMatching.mozilla } } + if (project.hasProperty("googleRepo")) { maven { name "Google" @@ -38,13 +40,13 @@ buildscript { } } - if (project.hasProperty("jcenterRepo")) { + if (project.hasProperty("centralRepo")) { maven { - name "BintrayJCenter" - url project.property("jcenterRepo") + name "MavenCentral" + url project.property("centralRepo") } } else { - jcenter() { + mavenCentral() { content { // Improve security: don't search deps with known repos. excludeGroupByRegex RepoMatching.mozilla @@ -86,6 +88,7 @@ allprojects { includeGroupByRegex RepoMatching.mozilla } } + maven { name "Mozilla" url "https://maven.mozilla.org/maven2" @@ -94,6 +97,7 @@ allprojects { includeGroupByRegex RepoMatching.mozilla } } + if (project.hasProperty("googleRepo")) { maven { name "Google" @@ -110,13 +114,14 @@ allprojects { } } } - if (project.hasProperty("jcenterRepo")) { + + if (project.hasProperty("centralRepo")) { maven { - name "BintrayJCenter" - url project.property("jcenterRepo") + name "MavenCentral" + url project.property("centralRepo") } } else { - jcenter() { + mavenCentral() { content { // Improve security: don't search deps with known repos. excludeGroupByRegex RepoMatching.mozilla @@ -127,6 +132,53 @@ allprojects { } } } + + if (project.hasProperty("jcenterRepo")) { + maven { + name "BintrayJCenter" + url project.property("jcenterRepo") + } + } else { + jcenter() { + content { + //////////////////////////////////////////////////////////////////////////////// + // JCenter is going away. Please do not add any new dependencies here. + // https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/ + //////////////////////////////////////////////////////////////////////////////// + + // Used by Android Gradle Plugin + // Issue for publishing to maven central: https://youtrack.jetbrains.com/issue/IDEA-261387 + // Related plugin issue: https://issuetracker.google.com/issues/179291081 + // Other option: Update to Android Gradle Plugin 7+ (still in Alpha currently) + includeVersion("org.jetbrains.trove4j", "trove4j", "20160824") + + // Used by detekt + // Issue for publishing to maven central: https://github.com/Kotlin/kotlinx.html/issues/173 + // Related detekt issue: https://github.com/detekt/detekt/issues/3461 + includeVersion("org.jetbrains.kotlinx", "kotlinx-html-jvm", "0.7.1") + includeVersion("org.jetbrains.kotlinx", "kotlinx-html-common", "0.7.1") + + // Fastlane + // Doesn't seem to be available on Maven Central yet, and I couldn't find an issue for it + // https://github.com/fastlane/fastlane + includeVersion("tools.fastlane", "screengrab", "2.0.0") + // https://github.com/jraska/Falcon/issues/52 + includeVersion("com.jraska", "falcon", "2.1.1") + } + } + } + + maven { + name "LeanplumRepo" + if (project.hasProperty("leanplumRepo")) { + url project.property("leanplumRepo") + } else { + url "https://repo.leanplum.com" + } + content { + includeGroup("com.leanplum") + } + } } tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt index dbe57e43d..5899e75df 100644 --- a/buildSrc/src/main/java/AndroidComponents.kt +++ b/buildSrc/src/main/java/AndroidComponents.kt @@ -3,5 +3,5 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object AndroidComponents { - const val VERSION = "72.0.16" + const val VERSION = "73.0.11" } diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index 65fd88c4f..6b103a612 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -3,8 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ object Versions { - const val kotlin = "1.4.10" - const val coroutines = "1.3.9" + const val kotlin = "1.4.30" + const val coroutines = "1.4.2" // These versions are linked: lint should be X+23.Y.Z of gradle_plugin version, according to: // https://github.com/alexjlockwood/android-lint-checks-demo/blob/0245fc027463137b1b4afb97c5295d60dce998b6/dependencies.gradle#L3 @@ -19,7 +19,7 @@ object Versions { const val jna = "5.6.0" const val androidx_appcompat = "1.2.0" - const val androidx_biometric = "1.1.0-rc01" + const val androidx_biometric = "1.1.0" const val androidx_coordinator_layout = "1.1.0" const val androidx_constraint_layout = "2.0.4" const val androidx_preference = "1.1.1" @@ -27,12 +27,12 @@ object Versions { const val androidx_annotation = "1.1.0" const val androidx_lifecycle = "2.2.0" const val androidx_fragment = "1.2.5" - const val androidx_navigation = "2.3.1" - const val androidx_recyclerview = "1.2.0-alpha06" + const val androidx_navigation = "2.3.3" + const val androidx_recyclerview = "1.2.0-beta01" const val androidx_core = "1.3.2" - const val androidx_paging = "2.1.0" - const val androidx_transition = "1.3.0" - const val androidx_work = "2.4.0" + const val androidx_paging = "2.1.2" + const val androidx_transition = "1.4.0" + const val androidx_work = "2.5.0" const val google_material = "1.2.1" const val mozilla_android_components = AndroidComponents.VERSION @@ -119,6 +119,7 @@ object Deps { const val mozilla_feature_accounts_push = "org.mozilla.components:feature-accounts-push:${Versions.mozilla_android_components}" const val mozilla_feature_top_sites = "org.mozilla.components:feature-top-sites:${Versions.mozilla_android_components}" const val mozilla_feature_share = "org.mozilla.components:feature-share:${Versions.mozilla_android_components}" + const val mozilla_feature_webauthn = "org.mozilla.components:feature-webauthn:${Versions.mozilla_android_components}" const val mozilla_feature_webcompat = "org.mozilla.components:feature-webcompat:${Versions.mozilla_android_components}" const val mozilla_feature_webnotifications = "org.mozilla.components:feature-webnotifications:${Versions.mozilla_android_components}" const val mozilla_feature_webcompat_reporter = "org.mozilla.components:feature-webcompat-reporter:${Versions.mozilla_android_components}" diff --git a/docs/metrics.md b/docs/metrics.md index 9d9b9eef3..0c988a1d7 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -28,8 +28,8 @@ an hashed version of the Google Advertising ID. **Bugs related to this ping:** -- 1538011 -- 1501822 +- +- The following metrics are added to the ping: @@ -52,9 +52,18 @@ The following metrics are added to the ping: | about_page.support_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped on "Support" item from About page |[1](https://github.com/mozilla-mobile/fenix/pull/8047), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 | | addons.open_addon_in_toolbar_menu |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user interacted with an installed add-on in the toolbar menu |[1](https://github.com/mozilla-mobile/fenix/pull/8318), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)|
  • addon_id: The id of the add-on that was interacted with in the toolbar menu
|2021-04-01 |2 | | addons.open_addons_in_settings |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user accessed "Add-ons" from the Settings |[1](https://github.com/mozilla-mobile/fenix/pull/8318), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 | +| android_keystore_experiment.experiment_failure |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Records an instance of an unexpected failure during the experiment |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)|
  • failure_exception: Exception class associated with an unexpected failure of this experiment, not caught by the other failure handlers.
|2021-09-01 |1 | +| android_keystore_experiment.get_failure |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Unexpected failure when trying to read from secure prefs. |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)|
  • failure_exception: Exception class associated with an unexpected failure of this experiment.
|2021-09-01 |1 | +| android_keystore_experiment.get_result |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Success when trying to read from secure prefs. |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)|
  • result: Result code identifying whether the read operation returned the expected value or not.
|2021-09-01 |1 | +| android_keystore_experiment.reset |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |An experiment failed, and was reset to run again in the future from a blank state. |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)||2021-09-01 |1 | +| android_keystore_experiment.write_failure |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Unexpected failure when trying to write to secure prefs. |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)|
  • failure_exception: Exception class associated with an unexpected failure of this experiment.
|2021-09-01 |1 | +| android_keystore_experiment.write_success |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Success in writing to secure prefs. |[1](https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395)||2021-09-01 |1 | | app_theme.dark_theme_selected |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user selected Dark Theme |[1](https://github.com/mozilla-mobile/fenix/pull/7968), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)|
  • source: The source from where dark theme was selected. The source can be 'SETTINGS' or 'ONBOARDING'
|2021-04-01 |2 | -| autoplay.setting_changed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user changed their autoplay setting to either block_cellular, block_audio, or block_all. |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)|
  • autoplay_setting: The new setting for autoplay: block_cellular, block_audio, or block_all.
|2021-02-01 |2 | -| autoplay.visited_setting |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user visited the autoplay settings screen |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)||2021-02-01 |2 | +| autoplay.setting_changed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user changed their autoplay setting to either block_cellular, block_audio, or block_all. |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)|
  • autoplay_setting: The new setting for autoplay: block_cellular, block_audio, or block_all.
|2021-08-01 |2 | +| autoplay.visited_setting |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user visited the autoplay settings screen |[1](https://github.com/mozilla-mobile/fenix/pull/13041#issuecomment-665777411)||2021-08-01 |2 | +| banner_open_in_app.dismissed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User tapped 'dismiss' on Open in App banner. |[1](https://github.com/mozilla-mobile/fenix/pull/17049)||2021-08-01 |2 | +| banner_open_in_app.displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |Open in App banner was shown. |[1](https://github.com/mozilla-mobile/fenix/pull/17049)||2021-08-01 |2 | +| banner_open_in_app.go_to_settings |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |User tapped 'go to settings' on Open in App banner. |[1](https://github.com/mozilla-mobile/fenix/pull/17049)||2021-08-01 |2 | | bookmarks_management.copied |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user copied a bookmark. |[1](https://github.com/mozilla-mobile/fenix/pull/1708), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 | | bookmarks_management.edited |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user edited the title and/or URL of an existing bookmark. |[1](https://github.com/mozilla-mobile/fenix/pull/1708), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 | | bookmarks_management.folder_add |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user added a new bookmark folder. |[1](https://github.com/mozilla-mobile/fenix/pull/1708), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 | @@ -128,10 +137,10 @@ The following metrics are added to the ping: | history.removed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user removed a history item |[1](https://github.com/mozilla-mobile/fenix/pull/3940), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | history.removed_all |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user removed all history items |[1](https://github.com/mozilla-mobile/fenix/pull/3940), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | history.shared |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user shared a history item |[1](https://github.com/mozilla-mobile/fenix/pull/3940), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | -| login_dialog.cancelled |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was cancelled |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 |2 | -| login_dialog.displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 |2 | -| login_dialog.never_save |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "never save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 |2 | -| login_dialog.saved |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-02-01 |2 | +| login_dialog.cancelled |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was cancelled |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-08-01 |2 | +| login_dialog.displayed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-08-01 |2 | +| login_dialog.never_save |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "never save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-08-01 |2 | +| login_dialog.saved |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The login dialog prompt "save" button was pressed |[1](https://github.com/mozilla-mobile/fenix/pull/13050)||2021-08-01 |2 | | logins.copy_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user copied a piece of a login in saved logins |[1](https://github.com/mozilla-mobile/fenix/pull/6352), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | logins.delete_saved_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user confirms delete of a saved login |[1](https://github.com/mozilla-mobile/fenix/issues/11208), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | logins.open_individual_login |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user accessed an individual login in saved logins |[1](https://github.com/mozilla-mobile/fenix/pull/6352), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | @@ -277,6 +286,10 @@ The following metrics are added to the ping: | browser.search.ad_clicks |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records clicks of adverts on SERP pages. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | browser.search.in_content |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records the type of interaction a user has on SERP pages. |[1](https://github.com/mozilla-mobile/fenix/pull/10167), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | browser.search.with_ads |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |Records counts of SERP pages with adverts displayed. The key format is ‘’. |[1](https://github.com/mozilla-mobile/fenix/pull/10112), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | +| engine.kill_background_age |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |Measures the age of the engine session of a background tab at the time its content process got killed. |[1](https://github.com/mozilla-mobile/fenix/pull/17864)||2021-12-31 |1 | +| engine.kill_foreground_age |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |Measures the age of the engine session of a foreground (selected) tab at the time its content process got killed. |[1](TBD)||2021-12-31 |1 | +| engine.tab_kills |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |How often was the content process of a foreground (selected) or background tab killed. |[1](https://github.com/mozilla-mobile/fenix/pull/17864)|
  • foreground
  • background
|2021-12-31 |1 | +| events.normal_and_private_uri_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter of URIs visited by the user in the current session, including page reloads. This includes private browsing. This does not include background page requests and URIs from embedded pages but may be incremented without user interaction by website scripts that programmatically redirect to a new location. |[1](https://github.com/mozilla-mobile/fenix/pull/17935)||2022-08-01 |2 | | events.total_uri_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter of URIs visited by the user in the current session, including page reloads. This does not include background page requests and URIs from embedded pages or private browsing but may be incremented without user interaction by website scripts that programmatically redirect to a new location. |[1](https://github.com/mozilla-mobile/fenix/pull/1785), [2](https://github.com/mozilla-mobile/fenix/pull/8314), [3](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | metrics.adjust_ad_group |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |A string containing the Adjust ad group ID from which the user installed Fenix. This will not send on the first session the user runs. If the install is organic, this will be empty. |[1](https://github.com/mozilla-mobile/fenix/pull/9253), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | metrics.adjust_campaign |[string](https://mozilla.github.io/glean/book/user/metrics/string.html) |A string containing the Adjust campaign ID from which the user installed Fenix. This will not send on the first session the user runs. If the install is organic, this will be empty. |[1](https://github.com/mozilla-mobile/fenix/pull/5579), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | @@ -308,6 +321,8 @@ The following metrics are added to the ping: | perf.awesomebar.session_suggestions |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |Duration of a session awesomebar suggestion query. |[1](https://github.com/mozilla-mobile/fenix/pull/10276#pullrequestreview-411101979)||2020-11-15 |1, 2 | | perf.awesomebar.shortcuts_suggestions |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |Duration of a shortcuts awesomebar suggestion query. |[1](https://github.com/mozilla-mobile/fenix/pull/10276#pullrequestreview-411101979)||2020-11-15 |1, 2 | | perf.awesomebar.synced_tabs_suggestions |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |Duration of a synced tabs awesomebar suggestion query. |[1](https://github.com/mozilla-mobile/fenix/pull/10276#pullrequestreview-411101979)||2020-11-15 |1, 2 | +| perf.startup.application_on_create |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |The duration of `FenixApplication.onCreate` in the main process. |[1](https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889)||2021-08-11 |1 | +| perf.startup.home_activity_on_create |[timing_distribution](https://mozilla.github.io/glean/book/user/metrics/timing_distribution.html) |The duration of `HomeActivity.onCreate`. |[1](https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889)||2021-08-11 |1 | | preferences.accessibility_services |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |Whether or not the user has touch exploration or switch services enabled. These are built into the Android OS, not Fenix prefs. default: "" |[1](https://github.com/mozilla-mobile/fenix/pull/11211), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | preferences.open_links_in_a_private_tab |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |Whether or not the user has enabled open links in a private tab. default: false |[1](https://github.com/mozilla-mobile/fenix/pull/11211), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | | preferences.open_links_in_app |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |Whether or not the user has the open links in apps feature enabled. default: false |[1](https://github.com/mozilla-mobile/fenix/pull/11446), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 | @@ -337,26 +352,34 @@ The following metrics are added to the ping: This ping is intended to provide an understanding of startup performance. -The ping is intended to be captured by performance testing automation to -report results there, in addition to user telemetry. We place these metrics -into their own ping in order to isolate them and make this process easier. +In addition to being captured on real devices, the ping data was prematurely +optimized into this separate ping to be isolated from other metrics to be +more easily captured by performance testing automation but that hasn't +happened in practice. We would have removed it but implementation +details don't make that possible: +https://github.com/mozilla-mobile/fenix/issues/17972#issuecomment-781002987 +This ping includes the [client id](https://mozilla.github.io/glean/book/user/pings/index.html#the-client_info-section). + **Data reviews for this ping:** - +- **Bugs related to this ping:** - +- The following metrics are added to the ping: | Name | Type | Description | Data reviews | Extras | Expiration | [Data Sensitivity](https://wiki.mozilla.org/Firefox/Data_Collection) | | --- | --- | --- | --- | --- | --- | --- | | startup.timeline.clock_ticks_per_second |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |The number of clock tick time units that occur in one second on this particular device. This value is expected to be used in conjunction with the `framework_start` metric. |[1](https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | -| startup.timeline.framework_start |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |The duration the Android framework takes to start before letting us run code in `*Application.init`. This is calculated from `appInitTimestamp - processStartTimestamp`. `processStartTimestamp` is derived from the clock tick time unit, which is expected to be less granular than nanoseconds. Therefore, we convert and round our timestamps to clock ticks before computing the difference and convert back to nanoseconds to report. For debugging purposes, `clock_ticks_per_second`, which may vary between devices, is also reported as a metric |[1](https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | -| startup.timeline.framework_start_error |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |An error when attempting to record `framework_start` - the application init timestamp returned a negative value - which is likely indicative of a bug in the implementation. |[1](https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | +| startup.timeline.framework_primary |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |The duration the Android framework takes to start before letting us run code in `*Application.init` when this device has `clock_ticks_per_second` equal to 100: if it's not equal to 100, then this value is captured in `framework_secondary`. We split this into two metrics to make it easier to analyze in GLAM. We split on 100 because when we did our initial brief analysis - https://sql.telemetry.mozilla.org/queries/75591 - the results for clocks ticks were overwhelmingly 100. The duration is calculated from `appInitTimestamp - processStartTimestamp`. `processStartTimestamp` is derived from the clock tick time unit, which is expected to be less granular than nanoseconds. Therefore, we convert and round our timestamps to clock ticks before computing the difference and convert back to nanoseconds to report. For debugging purposes, `clock_ticks_per_second`, which may vary between devices, is also reported as a metric |[1](https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068), [3](https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284)||2021-08-01 |1 | +| startup.timeline.framework_secondary |[timespan](https://mozilla.github.io/glean/book/user/metrics/timespan.html) |The duration the Android framework takes to start before letting us run code in `*Application.init` when this device has `clock_ticks_per_second` not equal to 100. For more details on this metric, see `framework_primary` |[1](https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284)||2021-08-01 |1 | +| startup.timeline.framework_start_error |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |An error when attempting to record `framework_primary/secondary` - the application init timestamp returned a negative value - which is likely indicative of a bug in the implementation. |[1](https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | | startup.timeline.framework_start_read_error |[boolean](https://mozilla.github.io/glean/book/user/metrics/boolean.html) |An error when attempting to read stats from /proc pseudo-filesystem - privacy managers can block access to reading these files - the application will catch a file reading exception. |[1](https://github.com/mozilla-mobile/fenix/pull/10481), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1 | diff --git a/l10n-release.toml b/l10n-release.toml index 8bc678f7f..acce5ec89 100644 --- a/l10n-release.toml +++ b/l10n-release.toml @@ -52,6 +52,7 @@ locales = [ "ka", "kab", "kk", + "kmr", "kn", "ko", "lij", @@ -82,6 +83,7 @@ locales = [ "te", "tg", "th", + "tl", "tr", "trs", "uk", diff --git a/l10n.toml b/l10n.toml index ef8242192..e74207276 100644 --- a/l10n.toml +++ b/l10n.toml @@ -13,6 +13,7 @@ locales = [ "bs", "ca", "cak", + "ceb", "co", "cs", "cy", @@ -42,6 +43,7 @@ locales = [ "gu-IN", "he", "hi-IN", + "hil", "hr", "hsb", "hu", diff --git a/taskcluster/ci/browsertime/kind.yml b/taskcluster/ci/browsertime/kind.yml index ea014660d..2c2f7d6a1 100644 --- a/taskcluster/ci/browsertime/kind.yml +++ b/taskcluster/ci/browsertime/kind.yml @@ -83,7 +83,6 @@ job-defaults: - '--cfg=mozharness/configs/raptor/android_hw_config.py' - '--app=fenix' - '--browsertime' - - '--cold' - '--no-conditioned-profile' - '--binary=org.mozilla.fenix' - '--activity=org.mozilla.fenix.IntentReceiverActivity' @@ -101,168 +100,54 @@ job-defaults: - linux64-node jobs: - tp6m-1-cold: - test-name: amazon - treeherder: - symbol: 'Btime(tp6m-1-c)' - - tp6m-2-cold: - test-name: google - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-2-c)' - - tp6m-3-cold: - test-name: instagram - treeherder: - symbol: 'Btime(tp6m-3-c)' - - tp6m-4-cold: - test-name: bing-search-restaurants - treeherder: - symbol: 'Btime(tp6m-4-c)' - - tp6m-5-cold: - test-name: ebay-kleinanzeigen-search - treeherder: - symbol: 'Btime(tp6m-5-c)' - - tp6m-6-cold: - test-name: amazon-search - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-6-c)' - - tp6m-7-cold: - test-name: wikipedia - treeherder: - symbol: 'Btime(tp6m-7-c)' - - tp6m-8-cold: - test-name: booking - treeherder: - symbol: 'Btime(tp6m-8-c)' - - tp6m-9-cold: - test-name: cnn-ampstories - treeherder: - symbol: 'Btime(tp6m-9-c)' - - tp6m-10-cold: - test-name: bbc - treeherder: - symbol: 'Btime(tp6m-10-c)' - - tp6m-11-cold: - test-name: microsoft-support - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-11-c)' - - tp6m-12-cold: - test-name: imdb - treeherder: - symbol: 'Btime(tp6m-12-c)' - - tp6m-13-cold: - test-name: espn - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-13-c)' - - tp6m-14-cold: - test-name: facebook-cristiano - treeherder: - symbol: 'Btime(tp6m-14-c)' - - tp6m-15-cold: - test-name: facebook - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-15-c)' - - tp6m-16-cold: - test-name: youtube - treeherder: - symbol: 'Btime(tp6m-16-c)' - - tp6m-17-cold: - test-name: bing - treeherder: - symbol: 'Btime(tp6m-17-c)' - - tp6m-18-cold: - test-name: ebay-kleinanzeigen - treeherder: - symbol: 'Btime(tp6m-18-c)' - - tp6m-19-cold: - test-name: google-maps - treeherder: - symbol: 'Btime(tp6m-19-c)' - - tp6m-20-cold: - test-name: youtube-watch + tp6m: + page-load-tests: + - amazon + - instagram + - [bing-search-restaurants, bing-s-r] + - [ebay-kleinanzeigen-search, ebay-k-s] + - wikipedia + - booking + - [cnn-ampstories, cnn-amp] + - bbc + - imdb + - [facebook-cristiano, fb-cris] + - youtube + - bing + - [ebay-kleinanzeigen, ebay-k] + - [google-maps, gmaps] + - reddit + - [stackoverflow, stacko] + - jianshu + - web-de + - cnn + - [google-search-restaurants, gsearch-r] + + tp6m-hv: web-render-only: False - treeherder: - symbol: 'Btime(tp6m-20-c)' - - tp6m-21-cold: - test-name: reddit - treeherder: - symbol: 'Btime(tp6m-21-c)' - - tp6m-22-cold: - test-name: stackoverflow - treeherder: - symbol: 'Btime(tp6m-22-c)' - - tp6m-23-cold: - test-name: jianshu - treeherder: - symbol: 'Btime(tp6m-23-c)' - - tp6m-24-cold: - test-name: allrecipes - web-render-only: False - treeherder: - symbol: 'Btime(tp6m-24-c)' - - tp6m-25-cold: - test-name: web-de - treeherder: - symbol: 'Btime(tp6m-25-c)' - - tp6m-27-cold: - test-name: cnn - treeherder: - symbol: 'Btime(tp6m-27-c)' - - tp6m-28-cold: - test-name: google-search-restaurants - treeherder: - symbol: 'Btime(tp6m-28-c)' + page-load-tests: + - google + - [amazon-search, amazon-s] + - [microsoft-support, micros-sup] + - espn + - facebook + - [youtube-watch, youtube-w] + - allrecipes youtube-playback-av1-sfr: description: "Raptor YouTube Playback AV1 SFR on Fenix" test-name: youtube-playback-av1-sfr run-visual-metrics: False chimera: False - treeherder: - symbol: 'Btime(ytp-av1-sfr)' youtube-playback-h264-sfr: description: "Raptor YouTube Playback H264 SFR on Fenix" test-name: youtube-playback-h264-sfr run-visual-metrics: False chimera: False - treeherder: - symbol: Btime(ytp-h264-sfr) youtube-playback-vp9-sfr: description: "Raptor YouTube Playback VP9 SFR on Fenix" run-visual-metrics: False chimera: False test-name: youtube-playback-vp9-sfr - treeherder: - symbol: Btime(ytp-vp9-sfr) diff --git a/taskcluster/ci/config.yml b/taskcluster/ci/config.yml index 2f2caf9d8..4fb15565d 100644 --- a/taskcluster/ci/config.yml +++ b/taskcluster/ci/config.yml @@ -17,7 +17,10 @@ treeherder: 'Rap-P': 'Raptor power tests' 'TL': 'Toolchain builds for Linux 64-bits' -task-priority: highest +task-priority: + by-project: + "fenix": highest + "staging-fenix": low taskgraph: register: fenix_taskgraph:register diff --git a/taskcluster/docker/visual-metrics/run-visual-metrics.py b/taskcluster/docker/visual-metrics/run-visual-metrics.py index 799207a8d..698d66237 100755 --- a/taskcluster/docker/visual-metrics/run-visual-metrics.py +++ b/taskcluster/docker/visual-metrics/run-visual-metrics.py @@ -171,7 +171,12 @@ def append_result(log, suites, test_name, name, result, extra_options): subtests = suites.setdefault( test_name, - {"name": orig_test_name, "subtests": {}, "extraOptions": extra_options}, + { + "name": orig_test_name, + "tags": extra_options + ["visual"], + "subtests": {}, + "extraOptions": extra_options, + }, )["subtests"] if name not in subtests: diff --git a/taskcluster/fenix_taskgraph/release_promotion.py b/taskcluster/fenix_taskgraph/release_promotion.py index 414e19131..902e7371b 100644 --- a/taskcluster/fenix_taskgraph/release_promotion.py +++ b/taskcluster/fenix_taskgraph/release_promotion.py @@ -17,6 +17,7 @@ from taskgraph.util.taskgraph import find_decision_task, find_existing_tasks_fro RELEASE_PROMOTION_PROJECTS = ( "https://github.com/mozilla-mobile/fenix", + "https://github.com/mozilla-releng/staging-fenix", ) diff --git a/taskcluster/fenix_taskgraph/transforms/browsertime.py b/taskcluster/fenix_taskgraph/transforms/browsertime.py index 9f5e91572..544d06860 100644 --- a/taskcluster/fenix_taskgraph/transforms/browsertime.py +++ b/taskcluster/fenix_taskgraph/transforms/browsertime.py @@ -11,6 +11,7 @@ from __future__ import absolute_import, print_function, unicode_literals import copy import json +from copy import deepcopy from taskgraph.transforms.base import TransformSequence from taskgraph.util.treeherder import inherit_treeherder_from_dep from taskgraph.util.schema import resolve_keyed_by @@ -18,6 +19,28 @@ from taskgraph.util.schema import resolve_keyed_by transforms = TransformSequence() +@transforms.add +def split_raptor_subtests(config, tests): + for test in tests: + # For tests that have 'page-load-tests' listed, we want to create a separate + # test job for every subtest (i.e. split out each page-load URL into its own job) + subtests = test.pop("page-load-tests", None) + if not subtests: + yield test + continue + + for subtest in subtests: + pageload_test = deepcopy(test) + + if isinstance(subtest, list): + pageload_test["test-name"] = subtest[0] + pageload_test["subtest-symbol"] = subtest[1] + else: + pageload_test["test-name"] = subtest + pageload_test["subtest-symbol"] = subtest + yield pageload_test + + @transforms.add def add_variants(config, tasks): only_types = config.config["only-for-build-types"] @@ -26,7 +49,7 @@ def add_variants(config, tasks): tests = list(tasks) for dep_task in config.kind_dependencies_tasks: - build_type = dep_task.attributes.get("build-type", '') + build_type = dep_task.attributes.get("build-type", "") if build_type not in only_types: continue @@ -91,6 +114,22 @@ def build_browsertime_task(config, tasks): task["run"]["command"].append("--test={}".format(test_name)) task["run"]["command"].extend(task.pop("args", [])) + # Setup treherder symbol + symbol = task.pop("subtest-symbol", None) + + # taskcluster is merging task attributes with the default ones + # resulting the --cold extra option in the ytp warm tasks + if "youtube-playback" in task["name"]: + symbol = test_name.replace("youtube-playback-", "ytp-") + + # Setup chimera for combined warm+cold testing + if task.pop("chimera", False): + task["run"]["command"].append("--chimera") + + # Add '-c' to taskcluster symbol when running cold tests + elif "--cold" in task["run"]["command"]: + symbol += "-c" + # Setup visual metrics run_visual_metrics = task.pop("run-visual-metrics", False) if run_visual_metrics: @@ -98,15 +137,11 @@ def build_browsertime_task(config, tasks): task["run"]["command"].append("--browsertime-no-ffwindowrecorder") task["attributes"]["run-visual-metrics"] = True - # Setup chimera for combined warm+cold testing - if task.pop("chimera", False): - task["run"]["command"].append("--chimera") - - # taskcluster is merging task attributes with the default ones - # resulting the --cold extra option in the ytp warm tasks - if 'youtube-playback' in task["name"]: - task["run"]["command"].remove("--cold") - + # Build taskcluster group and symol + task["treeherder"]["symbol"] = "Btime(%s)" % symbol + task["name"] = ( + task["name"].replace("tp6m-", "tp6m-{}-".format(symbol)).replace("-hv", "") + ) yield task @@ -129,7 +164,7 @@ def enable_webrender(config, tasks): @transforms.add def fill_email_data(config, tasks): - product_name = config.graph_config['taskgraph']['repositories']['mobile']['name'] + product_name = config.graph_config["taskgraph"]["repositories"]["mobile"]["name"] format_kwargs = { "product_name": product_name.lower(), "head_rev": config.params["head_rev"], @@ -138,7 +173,9 @@ def fill_email_data(config, tasks): for task in tasks: format_kwargs["task_name"] = task["name"] - resolve_keyed_by(task, 'notify', item_name=task["name"], level=config.params["level"]) + resolve_keyed_by( + task, "notify", item_name=task["name"], level=config.params["level"] + ) email = task["notify"].get("email") if email: email["link"]["href"] = email["link"]["href"].format(**format_kwargs) diff --git a/taskcluster/fenix_taskgraph/transforms/build.py b/taskcluster/fenix_taskgraph/transforms/build.py index a2ebdcf4f..032a746a6 100644 --- a/taskcluster/fenix_taskgraph/transforms/build.py +++ b/taskcluster/fenix_taskgraph/transforms/build.py @@ -104,7 +104,10 @@ def add_nightly_version(config, tasks): for task in tasks: if task.pop("include-nightly-version", False): - task["run"]["gradlew"].append('-PversionName={}'.format(formated_date_time)) + task["run"]["gradlew"].extend([ + '-PversionName={}'.format(formated_date_time), + '-Pofficial' + ]) yield task @@ -112,9 +115,10 @@ def add_nightly_version(config, tasks): def add_release_version(config, tasks): for task in tasks: if task.pop("include-release-version", False): - task["run"]["gradlew"].append( - '-PversionName={}'.format(config.params["version"]) - ) + task["run"]["gradlew"].extend([ + '-PversionName={}'.format(config.params["version"]), + '-Pofficial' + ]) yield task diff --git a/taskcluster/scripts/toolchain/android-gradle-dependencies.sh b/taskcluster/scripts/toolchain/android-gradle-dependencies.sh index 06c23c34a..ece18653a 100755 --- a/taskcluster/scripts/toolchain/android-gradle-dependencies.sh +++ b/taskcluster/scripts/toolchain/android-gradle-dependencies.sh @@ -19,7 +19,7 @@ pushd $PROJECT_DIR . taskcluster/scripts/toolchain/android-gradle-dependencies/before.sh NEXUS_PREFIX='http://localhost:8081/nexus/content/repositories' -GRADLE_ARGS="--parallel -PgoogleRepo=$NEXUS_PREFIX/google/ -PjcenterRepo=$NEXUS_PREFIX/jcenter/" +GRADLE_ARGS="--parallel -PgoogleRepo=$NEXUS_PREFIX/google/ -PjcenterRepo=$NEXUS_PREFIX/jcenter/ -PcentralRepo=$NEXUS_PREFIX/central/ -PleanplumRepo=$NEXUS_PREFIX/leanplum/" # We build everything to be sure to fetch all dependencies ./gradlew $GRADLE_ARGS assemble assembleAndroidTest testClasses ktlint detekt # Some tests may be flaky, although they still download dependencies. So we let the following diff --git a/taskcluster/scripts/toolchain/android-gradle-dependencies/after.sh b/taskcluster/scripts/toolchain/android-gradle-dependencies/after.sh index a62ab02a2..430974e5a 100644 --- a/taskcluster/scripts/toolchain/android-gradle-dependencies/after.sh +++ b/taskcluster/scripts/toolchain/android-gradle-dependencies/after.sh @@ -22,6 +22,8 @@ mkdir -p android-gradle-dependencies /builds/worker/artifacts cp -R ${NEXUS_WORK}/storage/jcenter android-gradle-dependencies cp -R ${NEXUS_WORK}/storage/google android-gradle-dependencies +cp -R ${NEXUS_WORK}/storage/central android-gradle-dependencies +cp -R ${NEXUS_WORK}/storage/leanplum android-gradle-dependencies tar cf - android-gradle-dependencies | xz > /builds/worker/artifacts/android-gradle-dependencies.tar.xz diff --git a/taskcluster/scripts/toolchain/android-gradle-dependencies/nexus.xml b/taskcluster/scripts/toolchain/android-gradle-dependencies/nexus.xml index 2b40bedf3..1dfc16205 100644 --- a/taskcluster/scripts/toolchain/android-gradle-dependencies/nexus.xml +++ b/taskcluster/scripts/toolchain/android-gradle-dependencies/nexus.xml @@ -54,6 +54,37 @@ true + + leanplum + leanplum + org.sonatype.nexus.proxy.repository.Repository + maven2 + IN_SERVICE + true + 1440 + true + true + true + READ_ONLY + true + true + + file + + + https://repo.leanplum.com/ + + + RELEASE + STRICT + true + false + -1 + 1440 + 1440 + true + + gradle-plugins Gradle Plugins diff --git a/version.txt b/version.txt index e09c8b8bc..275710fc9 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -86.1.1 +87.0.0-rc.1