Merge pull request #257 from fork-maintainers/beta-sync

Sync with Mozilla Firefox v84.1.2
pull/293/head iceraven-1.5.0
interfect 3 years ago committed by GitHub
commit 3e46fdc6f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -42,14 +42,17 @@
# Therefore, we make the Perfomance team code owners of this file. # Therefore, we make the Perfomance team code owners of this file.
/.github/CODEOWNERS @mozilla-mobile/Performance /.github/CODEOWNERS @mozilla-mobile/Performance
/app/src/*/java/org/mozilla/fenix/perf/** @mozilla-mobile/Performance # Own /perf/ src directories which typically includes perf code architecture
# or code that monitors for perf regressions. This is our main way to own code
# because it's simpler and less fragile than listing many specific files to own.
/**/src/**/perf/** @mozilla-mobile/Performance
# Possible regressions throughout the app
*.pro @mozilla-mobile/Performance *.pro @mozilla-mobile/Performance
*proguard* @mozilla-mobile/Performance *proguard* @mozilla-mobile/Performance
# Possible startup regressions # Possible startup regressions
*Application.kt @mozilla-mobile/Performance *Application.kt @mozilla-mobile/Performance
*StrictMode*kt @mozilla-mobile/Performance
*ConstraintLayoutPerfDetector.kt @mozilla-mobile/Performance
# We want to be aware of new features behind flags as well as features # We want to be aware of new features behind flags as well as features
# about to be enabled. # about to be enabled.

@ -1,67 +1,76 @@
## Overview ## ## Overview ##
Firefox for Android roughly follows the [Firefox Gecko release schedule](https://wiki.mozilla.org/Release_Management/Calendar#Calendars).
This means we cut a beta at the end of every two sprints, with a full cycle (~4 weeks) of baking on Beta before going to release. Uplifts must be approved by Release Owner (st3fan).
The [Firefox for Android release schedule](https://docs.google.com/spreadsheets/d/1HotjliSCGOp2nTkfXrxv8qYcurNpkqLWBKbbId6ovTY/edit#gid=0) contains more details related to specific Mobile handoffs.
| Monday | Tuesday | Wednesday | Thursday | Friday | | Monday | Tuesday | Wednesday | Thursday | Friday |
|-----------------|---------------------------|------------------------------|----------------|-------------| |-----------------|---------------------------|--------------------------------|-------------------|-----------------|
| (Sprint 1 Start)| | | | | | (week 1) | | (Y.2 sprint ended) |Sprint **X.1** starts | |
| | Hard code freeze for Beta | | | Code Freeze/Planning | (week 2) | | | | |
| Sprint 2 Start / Release to Beta / Release Production in Play Store 1% | QA Beta / Promote Release 25% | Promote Release 100% | | | | (week 3) | | Cut **X.1-beta** 12PST | **X.1-beta** QA / Sprint X.2 starts ||
| (week 4) | | | | |
| (week 5) | | Uplift L10N to **X.1-beta** | Sprint Z.1 starts | |
| (week 6) | Build X.1-RC with GV Prod for QA | | | |
| (week 7) | Release X.1 - 5% | Release X.1 20% / Cut Z.1-beta | Release X.1 100% | |
### Requirements ### Requirements
- Jira account - JIRA access
- Bugzilla account - Bugzilla account
- Google Play access (for reviewing crashes) - Sentry access
## Release Checklist ## Release Checklist
There are two releases this covers: the current sprint that is going out to Beta, and the previous beta that is going to Production. There are two releases this covers: the current sprint that is going out to Beta, and the previous Beta that is going to Production.
We will refer to the beta release going out as the *current* sprint release.
## Start of sprint [Monday, 1st week of sprint] ## Start of Sprint X.1 [Thursday, 1st week of sprint]
- [ ] Create milestone for *upcoming* sprint release. (e.g. if you are doing releng for v2.2, create v2.3 milestone) - [ ] [Create an issue](https://github.com/mozilla-mobile/fenix/issues/new?template=release_checklist.md&title=Releng+for+) "Releng for v[release]" to track the current sprint.
- [ ] If the upcoming release is a *major* (x.0) release, create an issue in the *upcoming* milestone: "What's New Entry for [*upcoming* release]" to track work for the SUMO page and Google Play release notes, e.g., if the current release is 2.3 but the upcoming one will be 3.0, make a "What's New" issue for 3.0. Product will use this to remember to check in with SUMO.
- [ ] [Create an issue](https://github.com/mozilla-mobile/fenix/issues/new?template=release_checklist.md&title=Releng+for+) in the *upcoming* milestone: "Releng for v[release]". Find an engineer who will handle the next releng task and assign them.
## Release Day [Monday, 3rd week] Beta & Production ## Sprint X.1 End [Wednesday, 2nd week] Cutting a Beta
- [ ] Promote previous Beta to Release
- [ ] Cherry-pick all merged [automated L10N string PRs](https://github.com/mozilla-mobile/fenix/pull/6156) to add newly translated strings and open a PR against branch that is going to release. (TODO is this safe?) This will require review.
- [ ] Tag the latest released RC version additionally with the tag of the release (v1.0-RC2 -> v1.0) (This can be done as soon as there are no more release blockers, does not need to be on Release Day.)
- [ ] **Verify that the commit hash of the new release matches the most recent RC.** This ensures that the correct version will be released
- [ ] Create a GitHub release build `vX.X.X` (v2.3.0) with the previous Beta branch as the target.
- [ ] Smoketest the signed build
- [ ] Load a url
- [ ] Set up sync
- [ ] Delete browsing data
- [ ] Upload the signed APK from the Taskcluster `signing-production` task to the [release page](https://github.com/mozilla-mobile/fenix/releases)
- [ ] Create a release request in Bugzilla to release to 1%. You can clone [this issue](https://bugzilla.mozilla.org/show_bug.cgi?id=1571967) and `need-info` someone from release management.
- [ ] Make a new Beta - [ ] Make a new Beta
- [ ] Create a branch off of master (DO NOT PUSH YET) for the *current* milestone of format `releases/v2.3` (where 2.3 is the *current* milestone). After that, anything landing in master will be part of the next milestone. - [ ] Create a branch off of master (DO NOT PUSH YET) for the *current* milestone of format `releases/v85.0.0`. After that, anything landing in master will be part of the next release.
- [ ] On the new Beta branch, pin the AC version to the stable version ([example](https://github.com/mozilla-mobile/fenix/commit/e413da29f6a7a7d4a765817a9cd5687abbf27619)) with commit message "Issue #`<this releng issue>`: Pin to stable AC `<version>` for release v2.3" (replacing 2.3 with the version) - [ ] On the new Beta branch, pin the AC version to the stable version ([example](https://github.com/mozilla-mobile/fenix/commit/e413da29f6a7a7d4a765817a9cd5687abbf27619)) with commit message "Issue #`<this releng issue>`: Pin to stable AC `<version>` for release v85"
- For each issue closed since the last release (run `kotlinc -script automation/releasetools/PrintMentionedIssuesAndPrs.kts` to get a list [see script for details] and paste it into the Releng issue): - [ ] Update the title to include this AC version "Releng for v[release] with AC [version]"
- [ ] Ensure it has the correct milestone.
- [ ] Add `eng:qa:needed` flags on each issue that still needs it.
- [ ] Go through the list of issues closed during this sprint in the Done column of the [Sprint Kanban](https://github.com/mozilla-mobile/fenix/projects/9) and make sure they all have the correct milestone.
- Note: You will need code review to make changes to the release branch after this point, because it is a protected branch. - Note: You will need code review to make changes to the release branch after this point, because it is a protected branch.
- [ ] Push the branch. - [ ] Push the branch.
- [ ] Create a GitHub pre-release build `vX.X.X-beta.1` (v2.3.0-beta.1) with the release branch as the target. This will kick off a build of the branch. You can see it in the mouseover of the CI badge of the branch in the commits view. Builds are found under `signing-*` task. - [ ] Create a GitHub pre-release [Release](https://github.com/mozilla-mobile/fenix/releases) with:
- If you need to trigger a new RC build, you will need to draft and publish a new (pre-release) release. Editing an existing release and creating a new tag will not trigger a new RC build. - [ ] Tag of the format `vX.X.X-beta.1` (v85.0.0-beta.1)
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] Create a new PI (product integrity) request in Jira. You can clone [this issue](https://jira.mozilla.com/browse/PI-219). - [ ] For the description of the release, look at the [Jira boards](https://jira.mozilla.com/secure/RapidBoard.jspa?rapidView=299&projectKey=FNX&view=reporting&chart=sprintRetrospective&sprint=883) for the X.1 and previous Y.2 sprints and list the major features that were added. This will help with the release notes later on.
- [ ] Click "Publish release". This will kick off a build of the branch. You can see it in the mouseover of the CI badge of the branch in the commits view. Builds are found under `signing-*` task.
### SUMO Verification [After Beta release] - If you need to trigger a new RC build, you **MUST** draft and publish a new (pre-release) release (optionally deleting both the release and the tag). Editing an existing release and creating a new tag will **not** trigger a new build.
- [ ] If the *current* release is a major (x.0) release, review the SUMO article contents of the whats new / other sumo pages and make sure they are accurate with what is in this release. If not, escalate to Product Owner. - [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.beta))
### During Beta Product Integrity (Beta Release until PI green signoff) ### Bugfix uplifts / Beta Product Integrity (Beta Release until PI green signoff)
- [ ] If bugs are considered release blocker then find someone to fix them on master and the milestone branch (cherry-pick / uplift) - [ ] If bugs are considered release blocker then find someone to fix them on master and the milestone branch (cherry-pick / uplift)
- [ ] If needed tag a new RC version (e.g. v1.0-RC2) and follow the submission checklist again. - [ ] Add the uplift request to the appropriate row in the [Uplifts document](https://docs.google.com/spreadsheets/d/1qIvHpcQ3BqJtlzV5T4M1MhbWVxkNiG-ToeYnWEBW4-I/edit#gid=0).
- [ ] If needed tag a new beta version (e.g. v1.0-beta.2) and follow the submission checklist again.
- [ ] Once there is GREEN QA signoff, file a [release management bugzilla for rollout](https://bugzilla.mozilla.org/show_bug.cgi?id=1664366)
- [ ] Check Sentry each day for issues on [Firefox Beta](https://sentry.prod.mozaws.net/operations/firefox-beta/) and if nothing concerning, bump release in the bugzilla (5%, 20%, 100%)
### During Production Release Rollout [Tuesday, Wednesday following Monday Release Day] ### Uplifting L10N strings to Beta [Wednesday, 2 weeks after sprint end]
- [ ] Check Sentry for new crashes. File issues and triage. - [ ] Find the issue ([example](https://github.com/mozilla-mobile/fenix/issues/16381)) filed by L10N / delphine saying string are ready for uplift (it takes 2 weeks for localizers to prepare localization).
- [ ] Ask Relman in the bug if they see potential blockers on Google Play, and if not, request that they bump the release each day (25% Tu, 100% Wed) - [ ] If there are new locales that are ready to be added to Release, add them to [l10n-release.toml](https://github.com/mozilla-mobile/fenix/blob/master/l10n-release.toml)
- [ ] Run the [L10N uplift script](https://github.com/mozilla-mobile/fenix/blob/master/l10n-uplift.py) against the releases/vX.1 branch (releases/v85.0.0). There will likely be conflicts, but if you are confused, they should match the strings in [main/Nightly](https://github.com/mozilla-mobile/fenix/tree/master/app/src/main/res)
- [ ] Once all conflicts are resolved, tag a new Beta to be released.
- [ ] Notify delphine in the L10N issue that the strings have been uplifted, and string quarantine can be lifted
### Production Release Candidate [Tuesday, 3 weeks after X.1 Beta was cut]
- [ ] In android-components: Create a dot release with the GeckoView Production release candidate.
- [ ] Open a PR against the release branch (releases/v85.0.0) with the AC version bump "Pin to stable AC `<version>` for release v85`. You will need code review.
- [ ] Create a GitHub pre-release [Release](https://github.com/mozilla-mobile/fenix/releases) with:
- [ ] Tag of the format `vX.X.X-rc.1` (v85.0.0-rc.1)
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] For the description, copy the beta description
- [ ] Send an email to QA at mozilla-mobile-qa@softvision.com with a link to the Taskcluster build (subdirectory of the [Fenix CI](https://firefox-ci-tc.services.mozilla.com/tasks/index/mobile.v2.fenix.release))
Major releases often need to be synchronized with other marketing activities (e.g. blog postings). ### Production Release [Release day, from [release calendar](https://docs.google.com/spreadsheets/d/1HotjliSCGOp2nTkfXrxv8qYcurNpkqLWBKbbId6ovTY/edit#gid=0)]
- [ ] Create a GitHub [Release](https://github.com/mozilla-mobile/fenix/releases) with:
- [ ] Tag of the format `vX.1.X` (v85.1.0) (increment the minor version for new cuts)
- [ ] The Target branch is the release branch (releases/v85.0.0)
- [ ] For the description, copy the beta description
- [ ] file Bugzilla ticket for [release manament](https://bugzilla.mozilla.org/show_bug.cgi?id=1672212)
## Room for improvement - [ ] Check Sentry for new crashes. File issues and triage.
- [ ] Automate assigning milestones to closed issues (based on date, etc) #6199 - [ ] Each day, bump the release rollout if nothing concerning (5%, 20%, 100%)
- [ ] Automate assignig `eng:qa:needed` to issues #6199
- [ ] Automate verification that the commit hash matches the most recent RC
- [ ] Builds generated as part of `signing-production` task look like `public/build/arm64-v8a/geckoBeta/target.apk`. This means that the dev must download, then rename them by hand. Could RM update these to generate `public/build/arm64-v8a/geckoBeta/firefox-preview-v3.0.0-rc.1-arm64-v8a.apk`, or similar?

@ -0,0 +1,23 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/
name: "Update Android-Components"
on:
schedule:
- cron: '*/15 * * * *'
jobs:
main:
name: "Update Android-Components"
runs-on: ubuntu-20.04
steps:
- name: "Update Android-Components"
uses: mozilla-mobile/relbot@master
if: github.repository == 'mozilla-mobile/fenix'
with:
project: fenix
command: update-android-components
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

@ -8,7 +8,7 @@ tasks:
- $let: - $let:
taskgraph: taskgraph:
branch: taskgraph branch: taskgraph
revision: 12992b0f984884ec2b0a7bdedc3b3ba467363eb4 revision: 2b2622598df02bde211d8cedb334b7b22fb883a4
trustDomain: mobile trustDomain: mobile
in: in:
$let: $let:
@ -104,7 +104,7 @@ tasks:
tasks_for in ["action", "cron"] tasks_for in ["action", "cron"]
|| (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"]) || (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")
|| (tasks_for == "github-release" && releaseAction == "published") || (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: then:
$let: $let:
level: level:
@ -243,7 +243,7 @@ tasks:
# Note: This task is built server side without the context or tooling that # Note: This task is built server side without the context or tooling that
# exist in tree so we must hard code the hash # exist in tree so we must hard code the hash
image: image:
mozillareleases/taskgraph:decision-mobile-6607973bc60e32323a541861cc5856cd6a0f51ea9fd664ef7d43bca8df53db47@sha256:8c471aacc469ea8e7bb4846c16efe086f7350a5cc1df570cc6c86b22895a2456 mozillareleases/taskgraph:decision-mobile-682fbaa1ef17e70ddfe3457da3eaf8e776c4a20fe5bfbdbeba0641fd5bceae2a@sha256:bbb2613aaab79d17e590fbd78c072d0643be40fd1237195703f84280ecc3b302
maxRunTime: 1800 maxRunTime: 1800
@ -261,12 +261,13 @@ tasks:
$if: 'tasks_for == "action"' $if: 'tasks_for == "action"'
then: > then: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph && PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh && taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts && ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph action-callback ~/.local/bin/taskgraph action-callback
else: > else: >
PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph && PIP_IGNORE_INSTALLED=0 pip install --user /builds/worker/checkouts/taskgraph &&
PIP_IGNORE_INSTALLED=0 pip install --user arrow taskcluster pyyaml && PIP_IGNORE_INSTALLED=0 pip install --user mozilla-version &&
taskcluster/scripts/decision-install-sdk.sh && taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts && ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph decision ~/.local/bin/taskgraph decision

@ -37,6 +37,13 @@ android {
manifestPlaceholders = [ manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue
] ]
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
if (project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")) {
buildConfigField "boolean", "MOZILLA_ONLINE", "true"
} else {
buildConfigField "boolean", "MOZILLA_ONLINE", "false"
}
} }
def releaseTemplate = { def releaseTemplate = {
@ -206,6 +213,17 @@ android {
testOptions { testOptions {
unitTests.returnDefaultValues = true unitTests.returnDefaultValues = true
unitTests.all {
// We keep running into memory issues when running our tests. With this config we
// reserve more memory and also create a new process after every 80 test classes. This
// is a band-aid solution and eventually we should try to find and fix the leaks
// instead. :)
maxParallelForks = 2
forkEvery = 80
maxHeapSize = "2048m"
minHeapSize = "1024m"
}
} }
} }

@ -433,21 +433,6 @@ onboarding:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- erichards@mozilla.com - erichards@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
whats_new:
type: event
description:
The onboarding What\'s New card was tapped.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/10824
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/11867
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
- erichards@mozilla.com
expires: "2021-08-01"
pref_toggled_theme_picker: pref_toggled_theme_picker:
type: event type: event
description: description:
@ -569,13 +554,15 @@ context_menu:
``` ```
open_in_new_tab, open_in_private_tab, open_image_in_new_tab, open_in_new_tab, open_in_private_tab, open_image_in_new_tab,
save_image, share_link, copy_link, copy_image_location save_image, share_link, copy_link, copy_image_location, share_image
``` ```
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/957 - https://github.com/mozilla-mobile/fenix/issues/957
- https://github.com/mozilla-mobile/fenix/issues/16076
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1344#issuecomment-479285010 - https://github.com/mozilla-mobile/fenix/pull/1344#issuecomment-479285010
- https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877 - https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877
- https://github.com/mozilla-mobile/fenix/issues/16076#issuecomment-726216734
data_sensitivity: data_sensitivity:
- interaction - interaction
notification_emails: notification_emails:
@ -698,6 +685,23 @@ metrics:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
distribution_id:
type: string
lifetime: application
description: |
A string containing the distribution identifier. This is currently used
to identify installs from Mozilla Online.
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16075
data_reviews:
- https://github.com/mozilla-mobile/fenix/issues/16075
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: never
top_sites_count: top_sites_count:
type: counter type: counter
lifetime: application lifetime: application
@ -1659,67 +1663,6 @@ activation:
no_lint: no_lint:
- USER_LIFETIME_EXPIRATION - USER_LIFETIME_EXPIRATION
qr_scanner:
opened:
type: event
description: |
A user opened the QR scanner
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1857
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
prompt_displayed:
type: event
description: |
A user scanned a QR code, causing a confirmation prompt to display asking
if they want to navigate to the page
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1857
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
navigation_allowed:
type: event
description: |
A user tapped "allow" on the prompt, directing the user to the website
scanned
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1857
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
navigation_denied:
type: event
description: |
A user tapped "deny" on the prompt, putting the user back to the scanning
view
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1857
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/2524#issuecomment-492739967
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
error_page: error_page:
visited_error: visited_error:
type: event type: event
@ -1858,22 +1801,6 @@ sync_auth:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
auto_login:
type: event
description: |
User signed into FxA via an account shared from another locally installed
Mozilla application (e.g. Fennec)
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4971
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/4931#issuecomment-529740300
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- technical
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
recovered: recovered:
type: event type: event
description: | description: |
@ -1936,20 +1863,6 @@ sync_account:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
closed:
type: event
description: |
A user closed the sync account page
bugs:
- https://github.com/mozilla-mobile/fenix/issues/1190
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/2745#issuecomment-494918532
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
sync_now: sync_now:
type: event type: event
description: | description: |
@ -2306,8 +2219,7 @@ tabs_tray:
save_to_collection: save_to_collection:
type: event type: event
description: | description: |
A user tapped the save to collection button in the A user tapped the save to collection button in the tabs tray
three dot menu within the tabs tray
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/11273 - https://github.com/mozilla-mobile/fenix/issues/11273
data_reviews: data_reviews:
@ -2591,81 +2503,7 @@ search_widget:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
search_widget_cfr:
displayed:
type: event
description: |
The search widget cfr was displayed.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9488
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10958
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
add_widget_pressed:
type: event
description: |
The user pressed the "add widget" button.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9488
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10958
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
not_now_pressed:
type: event
description: |
The user pressed the "not now" button.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9488
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10958
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
canceled:
type: event
description: |
The user dismissed the search widget cfr by
tapping outside of the prompt
bugs:
- https://github.com/mozilla-mobile/fenix/issues/9488
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/10958
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
private_browsing_mode: private_browsing_mode:
garbage_icon:
type: event
description: |
A user pressed the garbage can icon on the private browsing home page,
deleting all private tabs.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4658
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/4968
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
snackbar_undo: snackbar_undo:
type: event type: event
description: | description: |
@ -2695,35 +2533,6 @@ private_browsing_mode:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
notification_open:
type: event
description: |
A user pressed the private browsing mode notification's "Open" button.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4658
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/4968
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
notification_delete:
type: event
description: |
A user pressed the private browsing mode notification's "Delete and Open"
button.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/4658
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/4968
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
contextual_hint.tracking_protection: contextual_hint.tracking_protection:
display: display:

@ -110,7 +110,7 @@ class BaselinePingTest {
do { do {
attempts += 1 attempts += 1
val request = server.takeRequest(20L, TimeUnit.SECONDS) ?: break val request = server.takeRequest(20L, TimeUnit.SECONDS) ?: break
val docType = request.path.split("/")[3] val docType = request.path!!.split("/")[3]
if (pingName == docType) { if (pingName == docType) {
val parsedPayload = JSONObject(request.getPlainBody()) val parsedPayload = JSONObject(request.getPlainBody())
if (pingReason == null) { if (pingReason == null) {

@ -39,11 +39,13 @@ object MockWebServerHelper {
*/ */
fun createAlwaysOkMockWebServer(): MockWebServer { fun createAlwaysOkMockWebServer(): MockWebServer {
return MockWebServer().apply { return MockWebServer().apply {
setDispatcher(object : Dispatcher() { val dispatcher = object : Dispatcher() {
@Throws(InterruptedException::class)
override fun dispatch(request: RecordedRequest): MockResponse { override fun dispatch(request: RecordedRequest): MockResponse {
return MockResponse().setBody("OK") return MockResponse().setBody("OK")
} }
}) }
this.dispatcher = dispatcher
} }
} }
} }
@ -62,10 +64,10 @@ const val HTTP_NOT_FOUND = 404
class AndroidAssetDispatcher : Dispatcher() { class AndroidAssetDispatcher : Dispatcher() {
private val mainThreadHandler = Handler(Looper.getMainLooper()) private val mainThreadHandler = Handler(Looper.getMainLooper())
override fun dispatch(request: RecordedRequest?): MockResponse { override fun dispatch(request: RecordedRequest): MockResponse {
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
try { try {
val pathWithoutQueryParams = Uri.parse(request?.path?.drop(1)).path val pathWithoutQueryParams = Uri.parse(request.path!!.drop(1)).path
assetManager.open(pathWithoutQueryParams!!).use { inputStream -> assetManager.open(pathWithoutQueryParams!!).use { inputStream ->
return fileToResponse(pathWithoutQueryParams, inputStream) return fileToResponse(pathWithoutQueryParams, inputStream)
} }
@ -81,7 +83,7 @@ class AndroidAssetDispatcher : Dispatcher() {
private fun fileToResponse(path: String, file: InputStream): MockResponse { private fun fileToResponse(path: String, file: InputStream): MockResponse {
return MockResponse() return MockResponse()
.setResponseCode(HTTP_OK) .setResponseCode(HTTP_OK)
.setBody(fileToBytes(file)) .setBody(fileToBytes(file)!!)
.addHeader("content-type: " + contentType(path)) .addHeader("content-type: " + contentType(path))
} }

@ -0,0 +1,56 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.helpers.idlingresource
import android.view.View
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.IdlingResource.ResourceCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
class BottomSheetBehaviorStateIdlingResource(behavior: BottomSheetBehavior<*>) :
BottomSheetCallback(), IdlingResource {
private var isIdle: Boolean
private var callback: ResourceCallback? = null
override fun onStateChanged(bottomSheet: View, newState: Int) {
val wasIdle = isIdle
isIdle = isIdleState(newState)
if (!wasIdle && isIdle && callback != null) {
callback!!.onTransitionToIdle()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
// no-op
}
override fun getName(): String {
return BottomSheetBehaviorStateIdlingResource::class.java.simpleName
}
override fun isIdleNow(): Boolean {
return isIdle
}
override fun registerIdleTransitionCallback(callback: ResourceCallback) {
this.callback = callback
}
private fun isIdleState(state: Int): Boolean {
return state != BottomSheetBehavior.STATE_DRAGGING &&
state != BottomSheetBehavior.STATE_SETTLING &&
// When detecting STATE_HALF_EXPANDED we immediately transit to STATE_HIDDEN.
// Consider this also an intermediary state so not idling.
state != BottomSheetBehavior.STATE_HALF_EXPANDED
}
init {
behavior.addBottomSheetCallback(this)
val state = behavior.state
isIdle = isIdleState(state)
}
}

@ -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.matchers
import android.view.View
import androidx.test.espresso.matcher.BoundedMatcher
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.hamcrest.Description
class BottomSheetBehaviorStateMatcher(private val expectedState: Int) :
BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description?) {
description?.appendText("BottomSheetBehavior in state: \"$expectedState\"")
}
override fun matchesSafely(item: View): Boolean {
val behavior = BottomSheetBehavior.from(item)
return behavior.state == expectedState
}
}
class BottomSheetBehaviorHalfExpandedMaxRatioMatcher(private val maxHalfExpandedRatio: Float) :
BoundedMatcher<View, View>(View::class.java) {
override fun describeTo(description: Description?) {
description?.appendText(
"BottomSheetBehavior with an at max halfExpandedRation: " +
"$maxHalfExpandedRatio"
)
}
override fun matchesSafely(item: View): Boolean {
val behavior = BottomSheetBehavior.from(item)
return behavior.halfExpandedRatio <= maxHalfExpandedRatio
}
}

@ -0,0 +1,142 @@
/* 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.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import kotlinx.android.synthetic.main.activity_home.*
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.HomeActivityTestRule
// BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM.
private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val EXPECTED_RUNBLOCKING_COUNT = 2
private const val EXPECTED_COMPONENT_INIT_COUNT = 42
private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4
private val failureMsgStrictMode = getErrorMessage(
shortName = "StrictMode suppression",
implications = "suppressing a StrictMode violation can introduce performance regressions?"
)
private val failureMsgRunBlocking = getErrorMessage(
shortName = "runBlockingIncrement",
implications = "using runBlocking may block the main thread and have other negative performance implications?"
)
private val failureMsgComponentInit = getErrorMessage(
shortName = "Component init",
implications = "initializing new components on start up may be an indication that we're doing more work than necessary on start up?"
)
private val failureMsgViewHierarchyDepth = getErrorMessage(
shortName = "view hierarchy depth",
implications = "having a deep view hierarchy can slow down measure/layout performance?"
) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " +
"we'll find out over time if it is or is not."
private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
shortName = "ConstraintLayout being a common direct descendant of a RecyclerView",
implications = "ConstraintLayouts are slow to inflate and are primarily used to flatten deep " +
"view hierarchies so can be under-performant as a common RecyclerView child?"
) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " +
"we'll find out over time if it is or is not."
/**
* A performance test to limit the number of StrictMode suppressions and number of runBlocking used
* on startup.
*
* This test was written by the perf team.
*
* StrictMode detects main thread IO, which is often indicative of a performance issue.
* It's easy to suppress StrictMode so we wrote a test to ensure we have a discussion
* if the StrictMode count changes.
*
* RunBlocking is mostly used to return values to a thread from a coroutine. However, if that
* coroutine takes too long, it can lead that thread to block every other operations.
*
* The perf team is code owners for this package so they should be notified when the counts are modified.
*/
class StartupExcessiveResourceUseTest {
@get:Rule
val activityTestRule = HomeActivityTestRule(skipOnboarding = true)
private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@Test
fun verifyRunBlockingAndStrictModeSuppresionCount() {
uiDevice.waitForIdle() // wait for async UI to load.
// This might cause intermittents: at an arbitrary point after start up (such as the visual
// completeness queue), we might run code on the main thread that suppresses StrictMode,
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actualSuppresionCount = activityTestRule.activity.components.strictMode.suppressionCount.get().toInt()
val actualRunBlocking = RunBlockingCounter.count.get()
val actualComponentInitCount = ComponentInitCount.count.get()
val rootView = activityTestRule.activity.rootContainer
val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1)
val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount)
assertEquals(failureMsgViewHierarchyDepth, EXPECTED_VIEW_HIERARCHY_DEPTH, actualViewHierarchyDepth)
assertEquals(
failureMsgRecyclerViewConstraintLayoutChildren,
EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
actualRecyclerViewConstraintLayoutChildren
)
}
}
private fun countAndLogViewHierarchyDepth(view: View, level: Int): Int {
// Log for debugging purposes: not sure if this is actually helpful.
val indent = "| ".repeat(level - 1)
Log.d("Startup...Test", "${indent}$view")
return if (view !is ViewGroup) {
level
} else {
val maxDepth = view.children.map { countAndLogViewHierarchyDepth(it, level + 1) }.maxOrNull()
maxDepth ?: level
}
}
private fun countRecyclerViewConstraintLayoutChildren(view: View, parent: View?): Int {
val viewValue = if (parent is RecyclerView && view is ConstraintLayout) {
1
} else {
0
}
return if (view !is ViewGroup) {
viewValue
} else {
viewValue + view.children.sumBy { countRecyclerViewConstraintLayoutChildren(it, view) }
}
}
private fun getErrorMessage(shortName: String, implications: String) = """$shortName count does not match expected count.
If this PR removed a $shortName call, great! Please decrease the count.
Did this PR add or call code that increases the $shortName count?
Did you know that $implications
Please do your best to implement a solution without adding $shortName calls.
Please consult the perf team if you have questions or believe that having this call
is the optimal solution.
"""

@ -43,7 +43,7 @@ class MenuScreenShotTest : ScreenshotTest() {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -44,7 +44,7 @@ class SyncIntegrationTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -12,7 +12,6 @@ import mozilla.appservices.places.BookmarkRoot
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -49,7 +48,7 @@ class BookmarksTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -151,7 +150,6 @@ class BookmarksTest {
} }
} }
@Ignore("Flaky test, temp disabled: https://github.com/mozilla-mobile/fenix/issues/10690")
@Test @Test
fun editBookmarkTest() { fun editBookmarkTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -164,7 +162,6 @@ class BookmarksTest {
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) { }.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickEdit { }.clickEdit {
verifyEditBookmarksView() verifyEditBookmarksView()
verifyBookmarkNameEditBox() verifyBookmarkNameEditBox()
@ -173,9 +170,6 @@ class BookmarksTest {
changeBookmarkTitle(testBookmark.title) changeBookmarkTitle(testBookmark.title)
changeBookmarkUrl(testBookmark.url) changeBookmarkUrl(testBookmark.url)
saveEditBookmark() saveEditBookmark()
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkTitle(testBookmark.title) verifyBookmarkTitle(testBookmark.title)
verifyBookmarkedURL(testBookmark.url) verifyBookmarkedURL(testBookmark.url)
verifyKeyboardHidden() verifyKeyboardHidden()

@ -42,7 +42,7 @@ class ContextMenusTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -43,7 +43,7 @@ class DeepLinkTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -51,7 +51,7 @@ class DownloadTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -41,7 +41,7 @@ class HistoryTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -82,6 +82,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -101,6 +102,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -119,6 +121,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -140,6 +143,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -160,6 +164,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -180,6 +185,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -200,6 +206,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -221,6 +228,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -250,6 +258,7 @@ class HistoryTest {
homeScreen { }.openThreeDotMenu { homeScreen { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -273,6 +282,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -299,6 +309,7 @@ class HistoryTest {
}.submitQuery(secondWebPage.url.toString()) { }.submitQuery(secondWebPage.url.toString()) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list), 1)
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)
@ -326,6 +337,7 @@ class HistoryTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyHistoryListExists()
historyListIdlingResource = historyListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list)) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.history_list))
IdlingRegistry.getInstance().register(historyListIdlingResource!!) IdlingRegistry.getInstance().register(historyListIdlingResource!!)

@ -13,7 +13,6 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.ui.robots.PRIVATE_SESSION_MESSAGE
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
/** /**
@ -54,68 +53,6 @@ class HomeScreenTest {
} }
} }
@Test
fun firstRunScreenTest() {
homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
verifyWelcomeHeader()
// Sign in to Firefox
verifyGetTheMostHeader()
verifyAccountsSignInButton()
// Intro to other sections
verifyGetToKnowHeader()
// See What's new
// scrollToElementByText("See whats new")
// verifyWhatsNewHeader()
// verifyWhatsNewLink()
// Automatic privacy
scrollToElementByText("Automatic privacy")
verifyAutomaticPrivacyfHeader()
verifyTrackingProtectionToggle()
verifyAutomaticPrivacyText()
/* Check disable due to Firebase failures on Pixel 2 API 28
// Choose your theme
verifyChooseThemeHeader()
verifyChooseThemeText()
verifyDarkThemeDescription()
verifyDarkThemeToggle()
verifyLightThemeDescription()
verifyLightThemeToggle()
// Browse privately
scrollToElementByText("Open Settings")
verifyBrowsePrivatelyHeader()
verifyBrowsePrivatelyText()
*/
swipeToBottom()
// Take a position
scrollToElementByText("Take a position")
verifyTakePositionHeader()
verifyTakePositionElements()
// Your privacy
scrollToElementByText("Your privacy")
verifyYourPrivacyHeader()
verifyYourPrivacyText()
verifyPrivacyNoticeButton()
// Start Browsing
swipeToBottom()
verifyStartBrowsingButton()
}
}
@Test @Test
fun privateModeScreenItemsTest() { fun privateModeScreenItemsTest() {
homeScreen { }.dismissOnboarding() homeScreen { }.dismissOnboarding()
@ -136,7 +73,7 @@ class HomeScreenTest {
homeScreen { homeScreen {
// To deal with the race condition where multiple "add tab" buttons are present, // To deal with the race condition where multiple "add tab" buttons are present,
// we need to wait until previous HomeFragment View objects are gone. // we need to wait until previous HomeFragment View objects are gone.
mDevice.waitNotNull(Until.gone(By.text(PRIVATE_SESSION_MESSAGE)), waitingTime) mDevice.waitNotNull(Until.gone(By.text(privateSessionMessage)), waitingTime)
verifyHomeScreen() verifyHomeScreen()
verifyNavigationToolbar() verifyNavigationToolbar()
verifyHomePrivateBrowsingButton() verifyHomePrivateBrowsingButton()

@ -37,7 +37,7 @@ class MediaNotificationTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -38,7 +38,7 @@ class NavigationToolbarTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -41,7 +41,7 @@ class ReaderViewTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }

@ -33,7 +33,7 @@ class SettingsAboutTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -39,7 +39,7 @@ class SettingsAddonsTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -32,7 +32,7 @@ class SettingsAdvancedTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -39,7 +39,7 @@ class SettingsBasicsTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -204,7 +204,7 @@ class SettingsBasicsTest {
checkTextSizeOnWebsite(textSizePercentage, fenixApp.components) checkTextSizeOnWebsite(textSizePercentage, fenixApp.components)
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openAccessibilitySubMenu { }.openAccessibilitySubMenu {

@ -33,7 +33,7 @@ class SettingsDeveloperToolsTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -41,7 +41,7 @@ class SettingsPrivacyTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -197,7 +197,7 @@ class SettingsPrivacyTest {
saveLoginFromPrompt("Save") saveLoginFromPrompt("Save")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
TestHelper.scrollToElementByText("Logins and passwords") TestHelper.scrollToElementByText("Logins and passwords")
@ -223,7 +223,7 @@ class SettingsPrivacyTest {
saveLoginFromPrompt("Never save") saveLoginFromPrompt("Never save")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openLoginsAndPasswordSubMenu { }.openLoginsAndPasswordSubMenu {
@ -278,7 +278,7 @@ class SettingsPrivacyTest {
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyPrivateModeSelected() verifyPrivateModeSelected()
}.openNewTab { }.dismiss { } }.openNewTab { }.dismissSearchBar { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
@ -325,7 +325,7 @@ class SettingsPrivacyTest {
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut(pageShortcutName) { }.openHomeScreenShortcut(pageShortcutName) {
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.dismiss { } }.openNewTab { }.dismissSearchBar { }
setOpenLinksInPrivateOff() setOpenLinksInPrivateOff()
restartApp(activityTestRule) restartApp(activityTestRule)
@ -336,7 +336,7 @@ class SettingsPrivacyTest {
}.openTabDrawer { }.openTabDrawer {
verifyNormalModeSelected() verifyNormalModeSelected()
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openPrivateBrowsingSubMenu { }.openPrivateBrowsingSubMenu {

@ -32,7 +32,7 @@ class SettingsSyncTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -32,7 +32,7 @@ class SettingsTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -33,7 +33,7 @@ class ShareButtonTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }

@ -4,12 +4,12 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -34,7 +34,7 @@ class SmokeTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -44,6 +44,56 @@ class SmokeTest {
mockWebServer.shutdown() mockWebServer.shutdown()
} }
// copied over from HomeScreenTest
@Test
fun firstRunScreenTest() {
homeScreen {
verifyHomeScreen()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()
verifyHomeMenu()
verifyHomeWordmark()
verifyWelcomeHeader()
// Sign in to Firefox
verifyStartSyncHeader()
verifyAccountsSignInButton()
// Intro to other sections
verifyGetToKnowHeader()
// Automatic privacy
scrollToElementByText("Automatic privacy")
verifyAutomaticPrivacyHeader()
verifyTrackingProtectionToggle()
verifyAutomaticPrivacyText()
// Choose your theme
verifyChooseThemeHeader()
verifyChooseThemeText()
verifyDarkThemeDescription()
verifyDarkThemeToggle()
verifyLightThemeDescription()
verifyLightThemeToggle()
// Browse privately
verifyBrowsePrivatelyHeader()
verifyBrowsePrivatelyText()
// Take a position
verifyTakePositionHeader()
verifyTakePositionElements()
// Your privacy
verifyYourPrivacyHeader()
verifyYourPrivacyText()
verifyPrivacyNoticeButton()
// Start Browsing
verifyStartBrowsingButton()
}
}
@Test @Test
fun verifyBasicNavigationToolbarFunctionality() { fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -58,15 +108,14 @@ class SmokeTest {
}.openTabDrawer { }.openTabDrawer {
verifyExistingTabList() verifyExistingTabList()
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyHomeScreen() verifyHomeScreen()
} }
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
@Test @Test
fun verifyPageMainMenuItemsListInPortraitNormalModeTest() { fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// Add this to check openInApp and youtube is a default app available in every Android emulator/device // Add this to check openInApp and youtube is a default app available in every Android emulator/device
val youtubeUrl = "www.youtube.com" val youtubeUrl = "www.youtube.com"
@ -75,24 +124,16 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMainMenuItems() verifyThreeDotMainMenuItems()
verifySaveCollection()
}.clickAddOnsReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
}.openThreeDotMenu {
}.openHistory { }.openHistory {
verifyTestPageUrl(defaultWebPage.url) verifyHistoryMenuView()
}.goBackToBrowser { }.goBackToBrowser {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openBookmarks {
verifyBookmarksMenuView() verifyBookmarksMenuView()
verifyEmptyBookmarksList()
}.goBackToBrowser { }.goBackToBrowser {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSyncedTabs { }.openSyncedTabs {
verifyNavigationToolBarHeader() verifySyncedTabsMenuHeader()
verifySyncedTabsStatus()
}.goBack { }.goBack {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
@ -107,7 +148,7 @@ class SmokeTest {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesTabs(defaultWebPage.title) verifyExistingTopSitesTabs(defaultWebPage.title)
}.openTabDrawer { }.openTabDrawer {
}.openTab(defaultWebPage.title) { }.openTab(defaultWebPage.title) {
@ -120,83 +161,7 @@ class SmokeTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSaveToCollection { }.openSaveToCollection {
verifyCollectionNameTextField() verifyCollectionNameTextField()
}.goBackToBrowser { }.exitSaveCollection {
}.openThreeDotMenu {
}.bookmarkPage {
verifySnackBarText("Bookmark saved!")
}.openThreeDotMenu {
}.sharePage {
verifyShareAppsLayout()
}.closeShareDialogReturnToPage {
}.openThreeDotMenu {
}.refreshPage {
verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer {
closeTabViaXButton(defaultWebPage.title)
}.openNewTab {
}.submitQuery(youtubeUrl) {
verifyBlueDot()
}.openThreeDotMenu {
verifyOpenInAppButton()
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
@Test
fun verifyPageMainMenuItemsListInPortraitPrivateModeTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// Add this to check openInApp and also youtube is a default app available in every Android emulator/device
val youtubeUrl = "www.youtube.com"
homeScreen {
togglePrivateBrowsingModeOnOff()
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
verifyThreeDotMainMenuItems()
}.clickAddOnsReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
}.goBackToBrowser {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
verifyEmptyBookmarksList()
}.goBackToBrowser {
}.openThreeDotMenu {
}.openSyncedTabs {
verifyNavigationToolBarHeader()
verifySyncedTabsStatus()
}.goBack {
}.openThreeDotMenu {
}.openSettings {
verifySettingsView()
}.goBackToBrowser {
}.openThreeDotMenu {
}.openFindInPage {
verifyFindInPageSearchBarItems()
}.closeFindInPage {
}.openThreeDotMenu {
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
togglePrivateBrowsingModeOnOff()
verifyExistingTopSitesTabs(defaultWebPage.title)
togglePrivateBrowsingModeOnOff()
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
}.openThreeDotMenu {
}.openAddToHomeScreen {
verifyShortcutNameField(defaultWebPage.title)
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(defaultWebPage.title) {
}.openThreeDotMenu { }.openThreeDotMenu {
}.bookmarkPage { }.bookmarkPage {
verifySnackBarText("Bookmark saved!") verifySnackBarText("Bookmark saved!")
@ -207,20 +172,15 @@ class SmokeTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.refreshPage { }.refreshPage {
verifyUrl(defaultWebPage.url.toString()) verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer { }.openNavigationToolbar {
closeTabViaXButton(defaultWebPage.title) }.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
}.openNewTab {
}.submitQuery(youtubeUrl) {
verifyBlueDot()
}.openThreeDotMenu { }.openThreeDotMenu {
verifyOpenInAppButton() verifyOpenInAppButton()
} }
} }
}
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12899")
@Test @Test
fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() { fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { homeScreen {
@ -234,19 +194,11 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
verifyEnhancedTrackingProtectionPanelNotVisible() verifyEnhancedTrackingProtectionPanelNotVisible()
}.openThreeDotMenu { }.openThreeDotMenu {
}.clickAddOnsReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
verifyTabCounter("2")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openEnhancedTrackingProtectionSubMenu { }.openEnhancedTrackingProtectionSubMenu {
clickEnhancedTrackingProtectionDefaults() clickEnhancedTrackingProtectionDefaults()
}.goBackToHomeScreen { }.goBack {
}.openTabDrawer { }.goBackToBrowser {
}.openTab(defaultWebPage.title) {
clickEnhancedTrackingProtectionPanel() clickEnhancedTrackingProtectionPanel()
verifyEnhancedTrackingProtectionSwitch() verifyEnhancedTrackingProtectionSwitch()
// Turning off TP Switch results in adding the WebPage to exception list // Turning off TP Switch results in adding the WebPage to exception list

@ -42,7 +42,7 @@ class StrictEnhancedTrackingProtectionTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }

@ -1,58 +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.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.HomeActivityTestRule
// PLEASE CONSULT WITH PERF TEAM BEFORE CHANGING THIS VALUE.
private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val FAILURE_MSG = """StrictMode startup suppression count does not match expected count.
If this PR removed code that suppressed StrictMode, great! Please decrement the suppression count.
Did this PR add or call code that suppresses a StrictMode violation?
Did you know that suppressing a StrictMode violation can introduce performance regressions?
If so, please do your best to implement a solution without suppressing StrictMode.
Please consult the perf team if you have questions or believe suppressing StrictMode
is the optimal solution.
"""
/**
* A performance test to limit the number of StrictMode suppressions on startup.
* This test was written by the perf team.
*
* StrictMode detects main thread IO, which is often indicative of a performance issue.
* It's easy to suppress StrictMode so we wrote a test to ensure we have a discussion
* if the StrictMode count changes. The perf team is code owners for this file so they
* should be notified when the count is modified.
*
* IF YOU UPDATE THE TEST NAME, UPDATE CODE OWNERS.
*/
class StrictModeStartupSuppressionCountTest {
@get:Rule
val activityTestRule = HomeActivityTestRule(skipOnboarding = true)
private val uiDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@Test
fun verifyStrictModeSuppressionCount() {
uiDevice.waitForIdle() // wait for async UI to load.
// This might cause intermittents: at an arbitrary point after start up (such as the visual
// completeness queue), we might run code on the main thread that suppresses StrictMode,
// causing this number to fluctuate depending on device speed. We'll deal with it if it occurs.
val actual = activityTestRule.activity.components.strictMode.suppressionCount.toInt()
assertEquals(FAILURE_MSG, EXPECTED_SUPPRESSION_COUNT, actual)
}
}

@ -6,6 +6,7 @@ package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import com.google.android.material.bottomsheet.BottomSheetBehavior
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -49,7 +50,7 @@ class TabbedBrowsingTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -84,7 +85,7 @@ class TabbedBrowsingTest {
}.openTabsListThreeDotMenu { }.openTabsListThreeDotMenu {
verifyCloseAllTabsButton() verifyCloseAllTabsButton()
verifyShareTabButton() verifyShareTabButton()
verifySaveCollection() verifySelectTabs()
} }
} }
@ -126,7 +127,7 @@ class TabbedBrowsingTest {
}.openTabsListThreeDotMenu { }.openTabsListThreeDotMenu {
verifyCloseAllTabsButton() verifyCloseAllTabsButton()
verifyShareTabButton() verifyShareTabButton()
verifySaveCollection() verifySelectTabs()
}.closeAllTabs { }.closeAllTabs {
verifyNoTabsOpened() verifyNoTabsOpened()
} }
@ -187,7 +188,7 @@ class TabbedBrowsingTest {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_1")
}.openNewTab { }.openNewTab {
}.dismiss { } }.dismissSearchBar { }
} }
@Test @Test
@ -254,6 +255,31 @@ class TabbedBrowsingTest {
} }
} }
@Test
fun verifyTabTrayNotShowingStateHalfExpanded() {
homeScreen { }.dismissOnboarding()
navigationToolbar {
}.openTabTray {
verifyNoTabsOpened()
// With no tabs opened the state should be STATE_COLLAPSED.
verifyBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
// Need to ensure the halfExpandedRatio is very small so that when in STATE_HALF_EXPANDED
// the tabTray will actually have a very small height (for a very short time) akin to being hidden.
verifyHalfExpandedRatio()
}.clickTopBar {
}.waitForTabTrayBehaviorToIdle {
// Touching the topBar would normally advance the tabTray to the next state.
// We don't want that.
verifyBehaviorState(BottomSheetBehavior.STATE_COLLAPSED)
}.advanceToHalfExpandedState {
}.waitForTabTrayBehaviorToIdle {
// TabTray should not be displayed in STATE_HALF_EXPANDED.
// When advancing to this state it should immediately be hidden.
verifyTabTrayIsClosed()
}
}
@Test @Test
fun verifyEmptyTabTray() { fun verifyEmptyTabTray() {
homeScreen { }.dismissOnboarding() homeScreen { }.dismissOnboarding()
@ -286,7 +312,7 @@ class TabbedBrowsingTest {
verifyExistingOpenTabs(defaultWebPage.title) verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title) verifyCloseTabsButton(defaultWebPage.title)
}.openNewTab { }.openNewTab {
}.dismiss { } }.dismissSearchBar { }
} }
@Test @Test

@ -32,7 +32,7 @@ class ThreeDotMenuMainTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -69,13 +69,13 @@ class ThreeDotMenuMainTest {
verifyHelpUrl() verifyHelpUrl()
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openWhatsNew { }.openWhatsNew {
verifyWhatsNewURL() verifyWhatsNewURL()
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { } }.dismissSearchBar { }
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {

@ -36,7 +36,7 @@ class TopSitesTest {
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher()) dispatcher = AndroidAssetDispatcher()
start() start()
} }
} }
@ -59,7 +59,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
} }
@ -78,14 +78,14 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openTopSiteTabWithTitle(title = defaultWebPageTitle) { }.openTopSiteTabWithTitle(title = defaultWebPageTitle) {
verifyUrl(defaultWebPage.url.toString().replace("http://", "")) verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -109,7 +109,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -119,6 +119,31 @@ class TopSitesTest {
} }
} }
@Test
fun verifyRenameTopSite() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val defaultWebPageTitle = "Test_Page_1"
val defaultWebPageTitleNew = "Test_Page_2"
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
verifyAddFirefoxHome()
}.addToFirefoxHome {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
verifyTopSiteContextMenuItems()
}.renameTopSite(defaultWebPageTitleNew) {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitleNew)
}
}
@Test @Test
fun verifyRemoveTopSite() { fun verifyRemoveTopSite() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -132,7 +157,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
}.openNewTab { }.openNewTab {
}.dismiss { }.dismissSearchBar {
verifyExistingTopSitesList() verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle) verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) { }.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {

@ -27,7 +27,6 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By.res import androidx.test.uiautomator.By.res
import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
@ -44,7 +43,13 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/ */
class BookmarksRobot { class BookmarksRobot {
fun verifyBookmarksMenuView() = assertBookmarksView() fun verifyBookmarksMenuView() {
mDevice.findObject(
UiSelector().text("Bookmarks")
).waitForExists(waitingTime)
assertBookmarksView()
}
fun verifyEmptyBookmarksList() = assertEmptyBookmarksList() fun verifyEmptyBookmarksList() = assertEmptyBookmarksList()
@ -58,10 +63,7 @@ class BookmarksRobot {
} }
fun verifyBookmarkTitle(title: String) { fun verifyBookmarkTitle(title: String) {
mDevice.waitNotNull( mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
Until.findObject(text(title)),
TestAssetHelper.waitingTime
)
assertBookmarkTitle(title) assertBookmarkTitle(title)
} }
@ -164,7 +166,7 @@ class BookmarksRobot {
fun saveEditBookmark() { fun saveEditBookmark() {
saveBookmarkButton().click() saveBookmarkButton().click()
mDevice.waitNotNull(Until.findObject(text("Bookmarks"))) mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime)
} }
fun clickParentFolderSelector() = bookmarkFolderSelector().click() fun clickParentFolderSelector() = bookmarkFolderSelector().click()

@ -33,6 +33,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matchers.not
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -155,8 +156,6 @@ class BrowserRobot {
fun verifyMenuButton() = assertMenuButton() fun verifyMenuButton() = assertMenuButton()
fun verifyBlueDot() = assertBlueDot()
fun verifyNavURLBarItems() { fun verifyNavURLBarItems() {
verifyEnhancedTrackingOptions() verifyEnhancedTrackingOptions()
pressBack() pressBack()
@ -187,10 +186,10 @@ class BrowserRobot {
.perform(ViewActions.pressBack()) .perform(ViewActions.pressBack())
} }
fun clickEnhancedTrackingProtectionPanel() = enhancedTrackingProtectionPanel().click() fun clickEnhancedTrackingProtectionPanel() = enhancedTrackingProtectionIndicator().click()
fun verifyEnhancedTrackingProtectionPanelNotVisible() = fun verifyEnhancedTrackingProtectionPanelNotVisible() =
assertEnhancedTrackingProtectionPanelNotVisible() assertEnhancedTrackingProtectionIndicatorNotVisible()
fun clickContextOpenLinkInNewTab() { fun clickContextOpenLinkInNewTab() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -415,12 +414,11 @@ fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view))
private fun assertNavURLBar() = navURLBar() private fun assertNavURLBar() = navURLBar()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun enhancedTrackingProtectionPanel() = fun enhancedTrackingProtectionIndicator() =
onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator)) onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator))
private fun assertEnhancedTrackingProtectionPanelNotVisible() { private fun assertEnhancedTrackingProtectionIndicatorNotVisible() {
enhancedTrackingProtectionPanel() enhancedTrackingProtectionIndicator().check(matches(not(isDisplayed())))
.check(matches(withEffectiveVisibility(Visibility.GONE)))
} }
private fun assertEnhancedTrackingProtectionSwitch() { private fun assertEnhancedTrackingProtectionSwitch() {
@ -453,10 +451,3 @@ private fun mediaPlayerPlayButton() =
.className("android.widget.Button") .className("android.widget.Button")
.text("Play") .text("Play")
) )
private fun assertBlueDot() {
onView(withId(R.id.notification_dot))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun addOnsReportSiteIssue() = onView(withText("Report Site Issue"))

@ -39,6 +39,8 @@ class HistoryRobot {
assertEmptyHistoryView() assertEmptyHistoryView()
} }
fun verifyHistoryListExists() = assertHistoryListExists()
fun verifyVisitedTimeTitle() { fun verifyVisitedTimeTitle() {
mDevice.waitNotNull( mDevice.waitNotNull(
Until.findObject( Until.findObject(
@ -85,6 +87,8 @@ class HistoryRobot {
class Transition { class Transition {
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
@ -131,6 +135,9 @@ private fun assertEmptyHistoryView() =
) )
.check(matches(withText("No history here"))) .check(matches(withText("No history here")))
private fun assertHistoryListExists() =
mDevice.findObject(UiSelector().resourceId("R.id.history_list")).waitForExists(waitingTime)
private fun assertVisitedTimeTitle() = private fun assertVisitedTimeTitle() =
onView(withId(R.id.header_title)).check(matches(withText("Today"))) onView(withId(R.id.header_title)).check(matches(withText("Today")))

@ -7,6 +7,7 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap import android.graphics.Bitmap
import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
@ -36,14 +37,18 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers
import org.junit.Assert import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Search import org.mozilla.fenix.components.Search
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.matchers.hasItem import org.mozilla.fenix.helpers.matchers.hasItem
@ -53,6 +58,12 @@ import org.mozilla.fenix.helpers.withBitmapDrawable
* Implementation of Robot Pattern for the home screen menu. * Implementation of Robot Pattern for the home screen menu.
*/ */
class HomeScreenRobot { class HomeScreenRobot {
val privateSessionMessage =
"${appContext.appName} clears your search and browsing history from private tabs when you close them" +
" or quit the app. While this doesnt make you anonymous to websites or your internet" +
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
fun verifyNavigationToolbar() = assertNavigationToolbar() fun verifyNavigationToolbar() = assertNavigationToolbar()
fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar() fun verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen() fun verifyHomeScreen() = assertHomeScreen()
@ -72,7 +83,7 @@ class HomeScreenRobot {
// First Run elements // First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader() fun verifyWelcomeHeader() = assertWelcomeHeader()
fun verifyGetTheMostHeader() = assertGetTheMostHeader() fun verifyStartSyncHeader() = assertStartSyncHeader()
fun verifyAccountsSignInButton() = assertAccountsSignInButton() fun verifyAccountsSignInButton() = assertAccountsSignInButton()
fun verifyGetToKnowHeader() = assertGetToKnowHeader() fun verifyGetToKnowHeader() = assertGetToKnowHeader()
fun verifyChooseThemeHeader() = assertChooseThemeHeader() fun verifyChooseThemeHeader() = assertChooseThemeHeader()
@ -83,14 +94,10 @@ class HomeScreenRobot {
fun verifyDarkThemeDescription() = assertDarkThemeDescription() fun verifyDarkThemeDescription() = assertDarkThemeDescription()
fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle() fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle()
fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription() fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription()
fun verifyAutomaticPrivacyfHeader() = assertAutomaticPrivacyHeader() fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader()
fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle() fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle()
fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText() fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText()
// What's new elements
fun verifyWhatsNewHeader() = assertWhatsNewHeather()
fun verifyWhatsNewLink() = assertWhatsNewLink()
// Browse privately // Browse privately
fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader() fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader()
fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText() fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText()
@ -445,6 +452,18 @@ class HomeScreenRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun renameTopSite(title: String, interact: HomeScreenRobot.() -> Unit): Transition {
onView(withText("Rename"))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
.perform(click())
onView(Matchers.allOf(withId(R.id.top_site_title), instanceOf(EditText::class.java)))
.perform(ViewActions.replaceText(title))
onView(withId(android.R.id.button1)).perform((click()))
HomeScreenRobot().interact()
return Transition()
}
fun removeTopSite(interact: HomeScreenRobot.() -> Unit): Transition { fun removeTopSite(interact: HomeScreenRobot.() -> Unit): Transition {
onView(withText("Remove")) onView(withText("Remove"))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
@ -569,10 +588,10 @@ private fun verifySearchEngineIcon(searchEngineName: String) {
// First Run elements // First Run elements
private fun assertWelcomeHeader() = private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to Firefox Preview!"))) onView(allOf(withText("Welcome to ${appContext.appName}!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetTheMostHeader() = private fun assertStartSyncHeader() =
onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account."))) onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -581,51 +600,69 @@ private fun assertAccountsSignInButton() =
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() = private fun assertGetToKnowHeader() =
onView(allOf(withText("Get to know Firefox Preview"))) onView(allOf(withText("Get to know ${appContext.appName}")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertChooseThemeHeader() = private fun assertChooseThemeHeader() {
onView(allOf(withText("Choose your theme"))) scrollToElementByText("Choose your theme")
onView(withText("Choose your theme"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertChooseThemeText() = private fun assertChooseThemeText() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Save some battery and your eyesight by enabling dark mode."))) onView(allOf(withText("Save some battery and your eyesight by enabling dark mode.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeToggle() = private fun assertLightThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_light_radio_button")) onView(ViewMatchers.withResourceName("theme_light_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeDescription() = private fun assertLightThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Light theme"))) onView(allOf(withText("Light theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeToggle() = private fun assertDarkThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_dark_radio_button")) onView(ViewMatchers.withResourceName("theme_dark_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeDescription() = private fun assertDarkThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Dark theme"))) onView(allOf(withText("Dark theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticThemeToggle() = private fun assertAutomaticThemeToggle() {
scrollToElementByText("Choose your theme")
onView(withId(R.id.theme_automatic_radio_button)) onView(withId(R.id.theme_automatic_radio_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticThemeDescription() = private fun assertAutomaticThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Automatic"))) onView(allOf(withText("Automatic")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyHeader() = private fun assertAutomaticPrivacyHeader() {
scrollToElementByText("Automatic privacy")
onView(allOf(withText("Automatic privacy"))) onView(allOf(withText("Automatic privacy")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTrackingProtectionToggle() = onView( private fun assertTrackingProtectionToggle() {
allOf(ViewMatchers.withResourceName("tracking_protection_toggle")) scrollToElementByText("Automatic privacy")
) onView(withId(R.id.tracking_protection_toggle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyText() { private fun assertAutomaticPrivacyText() {
scrollToElementByText("Automatic privacy")
onView( onView(
allOf( allOf(
withText( withText(
@ -636,60 +673,65 @@ private fun assertAutomaticPrivacyText() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertBrowsePrivatelyHeader() = private fun assertBrowsePrivatelyHeader() {
scrollToElementByText("Browse privately")
onView(allOf(withText("Browse privately"))) onView(allOf(withText("Browse privately")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertBrowsePrivatelyText() = private fun assertBrowsePrivatelyText() {
scrollToElementByText("Browse privately")
onView(allOf(withText(containsString("Update your private browsing settings.")))) onView(allOf(withText(containsString("Update your private browsing settings."))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyHeader() = private fun assertYourPrivacyHeader() {
scrollToElementByText("Your privacy")
onView(allOf(withText("Your privacy"))) onView(allOf(withText("Your privacy")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyText() = private fun assertYourPrivacyText() {
scrollToElementByText("Your privacy")
onView( onView(
allOf( allOf(
withText( withText(
"Weve designed Firefox Preview to give you control over what you share online and what you share with us." "Weve designed ${appContext.appName} to give you control over what you share online and what you share with us."
) )
) )
) )
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertPrivacyNoticeButton() = private fun assertPrivacyNoticeButton() {
scrollToElementByText("Your privacy")
onView(allOf(withText("Read our privacy notice"))) onView(allOf(withText("Read our privacy notice")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// What's new elements private fun assertStartBrowsingButton() {
private fun assertWhatsNewHeather() = onView(allOf(withText("See whats new"))) scrollToElementByText("Start browsing")
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertWhatsNewLink() = onView(allOf(withText("Get answers here")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartBrowsingButton() =
onView(allOf(withText("Start browsing"))) onView(allOf(withText("Start browsing")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// Take a position // Take a position
private fun assertTakePositionheader() = onView(allOf(withText("Take a position"))) private fun assertTakePositionheader() {
scrollToElementByText("Take a position")
onView(allOf(withText("Take a position")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionTopRadioButton() = private fun assertTakePositionTopRadioButton() {
scrollToElementByText("Take a position")
onView(ViewMatchers.withResourceName("toolbar_top_radio_button")) onView(ViewMatchers.withResourceName("toolbar_top_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionBottomRadioButton() = private fun assertTakePositionBottomRadioButton() {
scrollToElementByText("Take a position")
onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button")) onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
const val PRIVATE_SESSION_MESSAGE =
"Firefox Preview clears your search and browsing history from private tabs when you close them" +
" or quit the app. While this doesnt make you anonymous to websites or your internet" +
" service provider, it makes it easier to keep what you do online private from anyone" +
" else who uses this device."
private fun assertPrivateSessionMessage() = private fun assertPrivateSessionMessage() =
onView(withId(R.id.private_session_description)) onView(withId(R.id.private_session_description))

@ -126,7 +126,7 @@ class SearchRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
fun dismiss(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { fun dismissSearchBar(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.waitForIdle() mDevice.waitForIdle()
mDevice.pressBack() mDevice.pressBack()
HomeScreenRobot().interact() HomeScreenRobot().interact()

@ -20,6 +20,7 @@ import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
@ -425,15 +426,17 @@ private fun assertAboutHeading(): ViewInteraction {
} }
private fun assertRateOnGooglePlay(): ViewInteraction { private fun assertRateOnGooglePlay(): ViewInteraction {
scrollToElementByText("About Firefox Preview") onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("Rate on Google Play"))))
return onView(withText("Rate on Google Play")) return onView(withText("Rate on Google Play"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertAboutFirefoxPreview(): ViewInteraction { private fun assertAboutFirefoxPreview(): ViewInteraction {
scrollToElementByText("About Firefox Preview") onView(withId(R.id.recycler_view))
.perform(RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(hasDescendant(withText("About Firefox Preview"))))
return onView(withText("About Firefox Preview")) return onView(withText("About Firefox Preview"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(isDisplayed()))
} }
fun swipeToBottom() = onView(withId(R.id.recycler_view)).perform(ViewActions.swipeUp()) fun swipeToBottom() = onView(withId(R.id.recycler_view)).perform(ViewActions.swipeUp())

@ -8,7 +8,6 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
@ -22,9 +21,7 @@ import org.mozilla.fenix.helpers.click
*/ */
class SyncedTabsRobot { class SyncedTabsRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader() fun verifySyncedTabsMenuHeader() = assertSyncedTabsMenuHeader()
fun verifySyncedTabsStatus() = assertSyncedTabsStatus()
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!! val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
@ -41,12 +38,7 @@ class SyncedTabsRobot {
private fun goBackButton() = private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up"))) onView(allOf(withContentDescription("Navigate up")))
private fun assertNavigationToolBarHeader() { private fun assertSyncedTabsMenuHeader() {
onView(withText(R.string.synced_tabs)) onView(withText(R.string.synced_tabs))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
} }
private fun assertSyncedTabsStatus() {
onView(withResourceName("sync_tabs_status"))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}

@ -7,13 +7,17 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.content.Context import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
@ -28,14 +32,19 @@ import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
/** /**
* Implementation of Robot Pattern for the home screen menu. * Implementation of Robot Pattern for the home screen menu.
@ -52,6 +61,10 @@ class TabDrawerRobot {
fun verifyNewTabButton() = assertNewTabButton() fun verifyNewTabButton() = assertNewTabButton()
fun verifyTabTrayOverflowMenu(visibility: Boolean) = assertTabTrayOverflowButton(visibility) fun verifyTabTrayOverflowMenu(visibility: Boolean) = assertTabTrayOverflowButton(visibility)
fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist()
fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio()
fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState)
fun closeTab() { fun closeTab() {
closeTabButton().click() closeTabButton().click()
} }
@ -126,9 +139,7 @@ class TabDrawerRobot {
fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle(waitingTime) mDevice.waitForIdle(waitingTime)
// Dismisses the tab tray bottom sheet with 2 handle clicks
onView(withId(R.id.handle)).perform( onView(withId(R.id.handle)).perform(
click(),
click() click()
) )
BrowserRobot().interact() BrowserRobot().interact()
@ -169,6 +180,52 @@ class TabDrawerRobot {
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun clickTopBar(interact: TabDrawerRobot.() -> Unit): Transition {
onView(withId(R.id.topBar)).click()
TabDrawerRobot().interact()
return Transition()
}
fun advanceToHalfExpandedState(interact: TabDrawerRobot.() -> Unit): Transition {
onView(withId(R.id.tab_wrapper)).perform(object : ViewAction {
override fun getDescription(): String {
return "Advance a BottomSheetBehavior to STATE_HALF_EXPANDED"
}
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(View::class.java)
}
override fun perform(uiController: UiController?, view: View?) {
val behavior = BottomSheetBehavior.from(view!!)
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
}
})
TabDrawerRobot().interact()
return Transition()
}
fun waitForTabTrayBehaviorToIdle(interact: TabDrawerRobot.() -> Unit): Transition {
var behavior: BottomSheetBehavior<*>? = null
onView(withId(R.id.tab_wrapper)).perform(object : ViewAction {
override fun getDescription(): String {
return "Postpone actions to after the BottomSheetBehavior has settled"
}
override fun getConstraints(): Matcher<View> {
return ViewMatchers.isAssignableFrom(View::class.java)
}
override fun perform(uiController: UiController?, view: View?) {
behavior = BottomSheetBehavior.from(view!!)
}
})
runWithIdleRes(BottomSheetBehaviorStateIdlingResource(behavior!!)) {
TabDrawerRobot().interact()
}
return Transition()
}
} }
} }
@ -239,6 +296,21 @@ private fun assertTabTrayOverflowButton(visible: Boolean) =
onView(withId(R.id.tab_tray_overflow)) onView(withId(R.id.tab_tray_overflow))
.check(matches(withEffectiveVisibility(visibleOrGone(visible)))) .check(matches(withEffectiveVisibility(visibleOrGone(visible))))
private fun assertTabTrayDoesNotExist() {
onView(withId(R.id.tab_wrapper))
.check(doesNotExist())
}
private fun assertMinisculeHalfExpandedRatio() {
onView(withId(R.id.tab_wrapper))
.check(matches(BottomSheetBehaviorHalfExpandedMaxRatioMatcher(0.001f)))
}
private fun assertBehaviorState(expectedState: Int) {
onView(withId(R.id.tab_wrapper))
.check(matches(BottomSheetBehaviorStateMatcher(expectedState)))
}
private fun tab(title: String) = private fun tab(title: String) =
onView( onView(
allOf( allOf(

@ -15,6 +15,7 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
@ -68,6 +69,7 @@ class ThreeDotMenuMainRobot {
fun verifyShareTabButton() = assertShareTabButton() fun verifyShareTabButton() = assertShareTabButton()
fun verifySaveCollection() = assertSaveCollectionButton() fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton()
fun clickBrowserViewSaveCollectionButton() { fun clickBrowserViewSaveCollectionButton() {
browserViewSaveCollectionButton().click() browserViewSaveCollectionButton().click()
@ -114,9 +116,11 @@ class ThreeDotMenuMainRobot {
fun verifyAddToMobileHome() = assertAddToMobileHome() fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite() fun verifyDesktopSite() = assertDesktopSite()
fun verifyOpenInAppButton() = assertOpenInAppButton() fun verifyOpenInAppButton() = assertOpenInAppButton()
fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyThreeDotMainMenuItems() { fun verifyThreeDotMainMenuItems() {
verifyAddOnsButton() verifyAddOnsButton()
verifyDownloadsButton()
verifyHistoryButton() verifyHistoryButton()
verifyBookmarksButton() verifyBookmarksButton()
verifySyncedTabsButton() verifySyncedTabsButton()
@ -125,6 +129,7 @@ class ThreeDotMenuMainRobot {
verifyAddFirefoxHome() verifyAddFirefoxHome()
verifyAddToMobileHome() verifyAddToMobileHome()
verifyDesktopSite() verifyDesktopSite()
verifySaveCollection()
verifyAddBookmarkButton() verifyAddBookmarkButton()
verifyShareButton() verifyShareButton()
verifyForwardButton() verifyForwardButton()
@ -135,14 +140,6 @@ class ThreeDotMenuMainRobot {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun clickAddOnsReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
addOnsButton().click()
addOnsReportSiteIssueButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings))) onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings)))
@ -224,13 +221,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun close(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { fun close(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
// Close three dot // Close three dot
mDevice.pressBack() mDevice.pressBack()
@ -359,6 +349,13 @@ class ThreeDotMenuMainRobot {
SettingsSubMenuAddonsManagerRobot().interact() SettingsSubMenuAddonsManagerRobot().interact()
return SettingsSubMenuAddonsManagerRobot.Transition() return SettingsSubMenuAddonsManagerRobot.Transition()
} }
fun exitSaveCollection(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
exitSaveCollectionButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
} }
} }
@ -372,9 +369,10 @@ private fun assertSettingsButton() = settingsButton()
.check(matches(isCompletelyDisplayed())) .check(matches(isCompletelyDisplayed()))
private fun addOnsButton() = onView(allOf(withText("Add-ons"))) private fun addOnsButton() = onView(allOf(withText("Add-ons")))
private fun assertAddOnsButton() = addOnsButton() private fun assertAddOnsButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
private fun addOnsReportSiteIssueButton() = onView(allOf(withText("Report Site Issue…"))) addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun historyButton() = onView(allOf(withText(R.string.library_history))) private fun historyButton() = onView(allOf(withText(R.string.library_history)))
private fun assertHistoryButton() = historyButton() private fun assertHistoryButton() = historyButton()
@ -397,8 +395,10 @@ private fun assertForwardButton() = forwardButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark")) private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark"))
private fun assertAddBookmarkButton() = addBookmarkButton() private fun assertAddBookmarkButton() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark")) private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark"))
private fun assertEditBookmarkButton() = editBookmarkButton() private fun assertEditBookmarkButton() = editBookmarkButton()
@ -431,6 +431,10 @@ private fun saveCollectionButton() = onView(allOf(withText("Save to collection")
private fun assertSaveCollectionButton() = saveCollectionButton() private fun assertSaveCollectionButton() = saveCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun selectTabsButton() = onView(allOf(withText("Select tabs"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertSelectTabsButton() = selectTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addNewCollectionButton() = onView(allOf(withText("Add new collection"))) private fun addNewCollectionButton() = onView(allOf(withText("Add new collection")))
private fun assertaddNewCollectionButton() = addNewCollectionButton() private fun assertaddNewCollectionButton() = addNewCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -454,10 +458,10 @@ private fun SendToDeviceTitle() =
private fun assertSendToDeviceTitle() = SendToDeviceTitle() private fun assertSendToDeviceTitle() = SendToDeviceTitle()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun ShareALinkTitle() = private fun shareALinkTitle() =
onView(allOf(withText("ALL ACTIONS"), withResourceName("apps_link_header"))) onView(allOf(withText("ALL ACTIONS"), withResourceName("apps_link_header")))
private fun assertShareALinkTitle() = ShareALinkTitle() private fun assertShareALinkTitle() = shareALinkTitle()
private fun whatsNewButton() = onView( private fun whatsNewButton() = onView(
allOf( allOf(
@ -511,12 +515,8 @@ private fun assertAddToMobileHome() {
private fun desktopSiteButton() = private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site))) onView(allOf(withText(R.string.browser_menu_desktop_site)))
private fun assertDesktopSite() { private fun assertDesktopSite() {
onView(withId(R.id.mozac_browser_menu_recyclerView)) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
.perform( desktopSiteButton().check(matches(isDisplayed()))
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.browser_menu_desktop_site))
)
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun openInAppButton() = private fun openInAppButton() =
@ -530,9 +530,15 @@ private fun assertOpenInAppButton() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun addonsManagerButton() = onView(withText("Add-ons Manager")) private fun downloadsButton() = onView(withText(R.string.library_downloads))
private fun assertDownloadsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
downloadsButton().check(matches(isDisplayed()))
}
private fun clickAddonsManagerButton() { private fun clickAddonsManagerButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
onView(withText("Add-ons")).check(matches(isCompletelyDisplayed())).click() addOnsButton().check(matches(isCompletelyDisplayed())).click()
} }
private fun exitSaveCollectionButton() = onView(withId(R.id.back_button)).check(matches(isDisplayed()))

@ -12,11 +12,17 @@ import mozilla.components.lib.crash.handler.CrashHandlerService
import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate import mozilla.components.service.sync.logins.GeckoLoginStorageDelegate
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
object GeckoProvider { object GeckoProvider {
private var runtime: GeckoRuntime? = 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 @Synchronized
fun getOrCreateRuntime( fun getOrCreateRuntime(
@ -53,6 +59,28 @@ object GeckoProvider {
runtimeSettings.fontSizeFactor = fontSize 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 geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage) val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)

@ -14,9 +14,15 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.GeckoRuntime import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
object GeckoProvider { object GeckoProvider {
private var runtime: GeckoRuntime? = 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 @Synchronized
fun getOrCreateRuntime( fun getOrCreateRuntime(
@ -53,6 +59,28 @@ object GeckoProvider {
runtimeSettings.fontSizeFactor = fontSize 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 geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage) val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)

@ -34,4 +34,9 @@
</body> </body>
<script src="./errorPageScripts.js"></script> <script src="./errorPageScripts.js"></script>
<script type="text/javascript">
if (window.history.length == 1) {
document.getElementById('backButton').style.display = 'none';
}
</script>
</html> </html>

@ -71,6 +71,10 @@
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
if (window.history.length == 1) {
document.getElementById('advancedPanelBackButton').style.display = 'none';
}
function toggleAdvancedAndScroll() { function toggleAdvancedAndScroll() {
toggleAdvanced(); toggleAdvanced();

@ -34,7 +34,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.R import mozilla.components.feature.addons.R
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.appName import mozilla.components.support.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.content.res.resolveAttribute
@ -158,7 +158,7 @@ class PagedAddonInstallationDialogFragment : AppCompatDialogFragment() {
rootView.findViewById<TextView>(R.id.title).text = rootView.findViewById<TextView>(R.id.title).text =
requireContext().getString( requireContext().getString(
R.string.mozac_feature_addons_installed_dialog_title, R.string.mozac_feature_addons_installed_dialog_title,
addon.translatedName, addon.translateName(requireContext()),
requireContext().appName requireContext().appName
) )

@ -35,8 +35,8 @@ import mozilla.components.feature.addons.ui.CustomViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.AddonViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.SectionViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import mozilla.components.feature.addons.ui.translatedSummary import mozilla.components.feature.addons.ui.translateSummary
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.content.res.resolveAttribute
import java.io.IOException import java.io.IOException
@ -96,7 +96,8 @@ class PagedAddonsManagerAdapter(
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.mozac_feature_addons_section_item, parent, false) val view = inflater.inflate(R.layout.mozac_feature_addons_section_item, parent, false)
val titleView = view.findViewById<TextView>(R.id.title) val titleView = view.findViewById<TextView>(R.id.title)
return SectionViewHolder(view, titleView) val divider = view.findViewById<View>(R.id.divider)
return SectionViewHolder(view, titleView, divider)
} }
private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder { private fun createUnsupportedSectionViewHolder(parent: ViewGroup): CustomViewHolder {
@ -210,13 +211,13 @@ class PagedAddonsManagerAdapter(
holder.titleView.text = holder.titleView.text =
if (addon.translatableName.isNotEmpty()) { if (addon.translatableName.isNotEmpty()) {
addon.translatedName addon.translateName(context)
} else { } else {
addon.id addon.id
} }
if (addon.translatableSummary.isNotEmpty()) { if (addon.translatableSummary.isNotEmpty()) {
holder.summaryView.text = addon.translatedSummary holder.summaryView.text = addon.translateSummary(context)
} else { } else {
holder.summaryView.visibility = View.GONE holder.summaryView.visibility = View.GONE
} }

@ -66,6 +66,13 @@ enum class ReleaseChannel {
ForkRelease -> true ForkRelease -> true
else -> false else -> false
} }
/**
* Is this a "Mozilla Online" build of Fenix? "Mozilla Online" is the Chinese branch of Mozilla
* and this flag will be `true` for builds shipping to Chinese app stores.
*/
val isMozillaOnline: Boolean
get() = BuildConfig.MOZILLA_ONLINE
} }
object Config { object Config {

@ -1,39 +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
import android.content.Context
import mozilla.components.service.experiments.Experiments
import org.mozilla.fenix.ext.settings
object ExperimentsManager {
fun optOutSearchWidgetExperiment(context: Context) {
// Release user has opted out of search widget CFR experiment, reset them to not see it.
context.settings().setSearchWidgetExperiment(false)
}
fun initSearchWidgetExperiment(context: Context) {
// When the `search-widget-discoverability` experiment is active,set the pref to either
// show or hide the search widget CFR (given other criteria are met as well).
// Note that this will not take effect the first time the application has launched,
// since there won't be enough time for the experiments library to get a list of experiments.
// It will take effect the second time the application is launched.
Experiments.withExperiment("fenix-search-widget") { branchName ->
when (branchName) {
"control_no_cfr" -> {
context.settings().setSearchWidgetExperiment(false)
}
"treatment_cfr" -> {
context.settings().setSearchWidgetExperiment(true)
}
else -> {
// No branch matches so we're defaulting to no CFR
context.settings().setSearchWidgetExperiment(false)
}
}
}
}
}

@ -21,16 +21,6 @@ object FeatureFlags {
*/ */
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Shows the grid view settings for the tabs tray.
*/
val showGridViewInTabsSettings = Config.channel.isNightlyOrDebug
/**
* Enables wait til first contentful paint
*/
const val waitUntilPaintToDraw = true
/** /**
* Enables downloads with external download managers. * Enables downloads with external download managers.
*/ */
@ -45,9 +35,4 @@ object FeatureFlags {
* Enables ETP cookie purging * Enables ETP cookie purging
*/ */
val etpCookiePurging = Config.channel.isNightlyOrDebug val etpCookiePurging = Config.channel.isNightlyOrDebug
/**
* Returns user to browser on cold start if they have open tabs
*/
val returnToBrowserOnColdStart = Config.channel.isNightlyOrDebug
} }

@ -19,7 +19,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.SystemAction
@ -44,6 +43,7 @@ import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
@ -137,7 +137,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// to invoke parts of itself that require complete megazord initialization // to invoke parts of itself that require complete megazord initialization
// before that process completes, we wait here, if necessary. // before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) { if (!megazordSetup.isCompleted) {
runBlocking { megazordSetup.await(); } runBlockingIncrement { megazordSetup.await() }
} }
} }
@ -171,24 +171,21 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
fun queueInitExperiments() { fun queueInitExperiments() {
@Suppress("ControlFlowWithEmptyBody")
if (settings().isExperimentationEnabled) { if (settings().isExperimentationEnabled) {
queue.runIfReadyOrQueue { queue.runIfReadyOrQueue {
Experiments.initialize( Experiments.initialize(
applicationContext = applicationContext, applicationContext = applicationContext,
onExperimentsUpdated = { onExperimentsUpdated = null,
ExperimentsManager.initSearchWidgetExperiment(this)
},
configuration = mozilla.components.service.experiments.Configuration( configuration = mozilla.components.service.experiments.Configuration(
httpClient = components.core.client, httpClient = components.core.client,
kintoEndpoint = KINTO_ENDPOINT_PROD kintoEndpoint = KINTO_ENDPOINT_PROD
) )
) )
ExperimentsManager.initSearchWidgetExperiment(this)
} }
} else { } else {
// We should make a better way to opt out for when we have more experiments // We should make a better way to opt out for when we have more experiments
// See https://github.com/mozilla-mobile/fenix/issues/6278 // See https://github.com/mozilla-mobile/fenix/issues/6278
ExperimentsManager.optOutSearchWidgetExperiment(this)
} }
} }
@ -446,10 +443,19 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// https://issuetracker.google.com/issues/143570309#comment3 // https://issuetracker.google.com/issues/143570309#comment3
applicationContext.resources.configuration.uiMode = config.uiMode applicationContext.resources.configuration.uiMode = config.uiMode
// random StrictMode onDiskRead violation even when Fenix is not running in the background. if (isMainProcess()) {
// We can only do this on the main process as resetAfter will access components.core, which
// will initialize the engine and create an additional GeckoRuntime from the Gecko
// child process, causing a crash.
// There's a strict mode violation in A-Cs LocaleAwareApplication which
// reads from shared prefs: https://github.com/mozilla-mobile/android-components/issues/8816
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
super.onConfigurationChanged(config) super.onConfigurationChanged(config)
} }
} else {
super.onConfigurationChanged(config)
}
} }
companion object { companion object {

@ -180,7 +180,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
.attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer)) .attachViewToRunVisualCompletenessQueueLater(WeakReference(rootContainer))
} }
checkPrivateShortcutEntryPoint(intent)
privateNotificationObserver = PrivateNotificationFeature( privateNotificationObserver = PrivateNotificationFeature(
applicationContext, applicationContext,
components.core.store, components.core.store,
@ -397,8 +396,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
*/ */
final override fun onNewIntent(intent: Intent?) { final override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent) super.onNewIntent(intent)
intent ?: return intent?.let {
handleNewIntent(it)
}
}
open fun handleNewIntent(intent: Intent) {
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960 // https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb( breadcrumb(
@ -599,17 +602,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
return false return false
} }
private fun checkPrivateShortcutEntryPoint(intent: Intent) {
if (intent.hasExtra(OPEN_TO_SEARCH) &&
(intent.getStringExtra(OPEN_TO_SEARCH) ==
StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB ||
intent.getStringExtra(OPEN_TO_SEARCH) ==
StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT)
) {
PrivateNotificationService.isStartedFromPrivateShortcut = true
}
}
private fun setupThemeAndBrowsingMode(mode: BrowsingMode) { private fun setupThemeAndBrowsingMode(mode: BrowsingMode) {
settings().lastKnownMode = mode settings().lastKnownMode = mode
browsingModeManager = createBrowsingModeManager(mode) browsingModeManager = createBrowsingModeManager(mode)
@ -778,8 +770,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
open fun navigateToBrowserOnColdStart() { open fun navigateToBrowserOnColdStart() {
// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
if (FeatureFlags.returnToBrowserOnColdStart && if (settings().shouldReturnToBrowser &&
settings().shouldReturnToBrowser &&
!browsingModeManager.mode.isPrivate !browsingModeManager.mode.isPrivate
) { ) {
openToBrowser(BrowserDirection.FromGlobal, null) openToBrowser(BrowserDirection.FromGlobal, null)

@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.showInformationDialog import mozilla.components.feature.addons.ui.showInformationDialog
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemptStorage import mozilla.components.feature.addons.update.DefaultAddonUpdater.UpdateAttemptStorage
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -34,7 +34,10 @@ class AddonDetailsFragment : Fragment(R.layout.fragment_add_on_details), AddonDe
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
showToolbar(title = args.addon.translatedName) context?.let {
showToolbar(title = args.addon.translateName(it))
}
AddonDetailsView(view, interactor = this).bind(args.addon) AddonDetailsView(view, interactor = this).bind(args.addon)
} }

@ -16,7 +16,7 @@ import androidx.core.text.getSpans
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_add_on_details.* import kotlinx.android.synthetic.main.fragment_add_on_details.*
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.translatedDescription import mozilla.components.feature.addons.ui.translateDescription
import mozilla.components.feature.addons.ui.updatedAtDate import mozilla.components.feature.addons.ui.updatedAtDate
import org.mozilla.fenix.R import org.mozilla.fenix.R
import java.text.DateFormat import java.text.DateFormat
@ -100,7 +100,7 @@ class AddonDetailsView(
} }
private fun bindDetails(addon: Addon) { private fun bindDetails(addon: Addon) {
val detailsText = addon.translatedDescription val detailsText = addon.translateDescription(containerView.context)
val parsedText = detailsText.replace("\n", "<br/>") val parsedText = detailsText.replace("\n", "<br/>")
val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT) val text = HtmlCompat.fromHtml(parsedText, HtmlCompat.FROM_HTML_MODE_COMPACT)

@ -11,7 +11,7 @@ import android.view.ViewGroup
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.* import kotlinx.android.synthetic.main.fragment_add_on_internal_settings.*
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
@ -33,7 +33,9 @@ class AddonInternalSettingsFragment : AddonPopupBaseFragment() {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
showToolbar(args.addon.translatedName) context?.let {
showToolbar(args.addon.translateName(it))
}
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

@ -9,7 +9,7 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -25,7 +25,9 @@ class AddonPermissionsDetailsFragment : Fragment(R.layout.fragment_add_on_permis
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
showToolbar(args.addon.translatedName) context?.let {
showToolbar(args.addon.translateName(it))
}
AddonPermissionsDetailsView(view, interactor = this).bind(args.addon) AddonPermissionsDetailsView(view, interactor = this).bind(args.addon)
} }

@ -33,7 +33,7 @@ import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.PermissionsDialogFragment import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment
import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -45,12 +45,13 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale import java.util.Locale
import java.lang.ref.WeakReference
import java.util.concurrent.CancellationException import java.util.concurrent.CancellationException
/** /**
* Fragment use for managing add-ons. * Fragment use for managing add-ons.
*/ */
@Suppress("LargeClass", "TooManyFunctions") @Suppress("TooManyFunctions", "LargeClass")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) { class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
/** /**
@ -243,7 +244,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun showInstallationDialog(addon: Addon) { private fun showInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonInstallationDialogFragment()) { if (!isInstallationInProgress && !hasExistingAddonInstallationDialogFragment()) {
requireComponents.analytics.metrics.track(Event.AddonInstalled(addon.id)) requireComponents.analytics.metrics.track(Event.AddonInstalled(addon.id))
val addonCollectionProvider = requireContext().components.addonCollectionProvider val context = requireContext()
val addonCollectionProvider = context.components.addonCollectionProvider
// Fragment may not be attached to the context anymore during onConfirmButtonClicked handling,
// but we still want to be able to process user selection of the 'allowInPrivateBrowsing' pref.
// This is a best-effort attempt to do so - retain a weak reference to the application context
// (to avoid a leak), which we attempt to use to access addonManager.
// See https://github.com/mozilla-mobile/fenix/issues/15816
val weakApplicationContext: WeakReference<Context> = WeakReference(context)
val dialog = PagedAddonInstallationDialogFragment.newInstance( val dialog = PagedAddonInstallationDialogFragment.newInstance(
addon = addon, addon = addon,
@ -263,7 +272,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
), ),
onConfirmButtonClicked = { _, allowInPrivateBrowsing -> onConfirmButtonClicked = { _, allowInPrivateBrowsing ->
if (allowInPrivateBrowsing) { if (allowInPrivateBrowsing) {
requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing( weakApplicationContext.get()?.components?.addonManager?.setAddonAllowedInPrivateBrowsing(
addon, addon,
allowInPrivateBrowsing, allowInPrivateBrowsing,
onSuccess = { onSuccess = {
@ -304,14 +313,16 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
// No need to display an error message if installation was cancelled by the user. // No need to display an error message if installation was cancelled by the user.
if (e !is CancellationException) { if (e !is CancellationException) {
val rootView = activity?.getRootView() ?: view val rootView = activity?.getRootView() ?: view
context?.let {
showSnackBar( showSnackBar(
rootView, rootView,
getString( getString(
R.string.mozac_feature_addons_failed_to_install, R.string.mozac_feature_addons_failed_to_install,
addon.translatedName addon.translateName(it)
) )
) )
} }
}
addonProgressOverlay?.visibility = View.GONE addonProgressOverlay?.visibility = View.GONE
isInstallationInProgress = false isInstallationInProgress = false
} }

@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translatedName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -85,7 +85,7 @@ class InstalledAddonDetailsFragment : Fragment() {
} }
private fun bindUI(view: View) { private fun bindUI(view: View) {
val title = addon.translatedName val title = addon.translateName(view.context)
showToolbar(title) showToolbar(title)
bindEnableSwitch(view) bindEnableSwitch(view)
@ -117,29 +117,33 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setText(R.string.mozac_feature_addons_enabled) switch.setText(R.string.mozac_feature_addons_enabled)
view.settings.isVisible = shouldSettingsBeVisible() view.settings.isVisible = shouldSettingsBeVisible()
view.remove_add_on.isEnabled = true view.remove_add_on.isEnabled = true
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_successfully_enabled, R.string.mozac_feature_addons_successfully_enabled,
addon.translatedName addon.translateName(it)
) )
) )
} }
}
}, },
onError = { onError = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
switch.isClickable = true switch.isClickable = true
view.remove_add_on.isEnabled = true view.remove_add_on.isEnabled = true
switch.setState(addon.isEnabled()) switch.setState(addon.isEnabled())
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_failed_to_enable, R.string.mozac_feature_addons_failed_to_enable,
addon.translatedName addon.translateName(it)
) )
) )
} }
} }
}
) )
} else { } else {
view.settings.isVisible = false view.settings.isVisible = false
@ -152,14 +156,16 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isVisible = it.isEnabled() privateBrowsingSwitch.isVisible = it.isEnabled()
switch.setText(R.string.mozac_feature_addons_disabled) switch.setText(R.string.mozac_feature_addons_disabled)
view.remove_add_on.isEnabled = true view.remove_add_on.isEnabled = true
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_successfully_disabled, R.string.mozac_feature_addons_successfully_disabled,
addon.translatedName addon.translateName(it)
) )
) )
} }
}
}, },
onError = { onError = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
@ -167,15 +173,17 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isClickable = true privateBrowsingSwitch.isClickable = true
view.remove_add_on.isEnabled = true view.remove_add_on.isEnabled = true
switch.setState(addon.isEnabled()) switch.setState(addon.isEnabled())
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_failed_to_disable, R.string.mozac_feature_addons_failed_to_disable,
addon.translatedName addon.translateName(it)
) )
) )
} }
} }
}
) )
} }
} }
@ -263,28 +271,32 @@ class InstalledAddonDetailsFragment : Fragment() {
onSuccess = { onSuccess = {
runIfFragmentIsAttached { runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true) setAllInteractiveViewsClickable(view, true)
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_successfully_uninstalled, R.string.mozac_feature_addons_successfully_uninstalled,
addon.translatedName addon.translateName(it)
) )
) )
}
view.findNavController().popBackStack() view.findNavController().popBackStack()
} }
}, },
onError = { _, _ -> onError = { _, _ ->
runIfFragmentIsAttached { runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true) setAllInteractiveViewsClickable(view, true)
context?.let {
showSnackBar( showSnackBar(
view, view,
getString( getString(
R.string.mozac_feature_addons_failed_to_uninstall, R.string.mozac_feature_addons_failed_to_uninstall,
addon.translatedName addon.translateName(it)
) )
) )
} }
} }
}
) )
} }
} }

@ -14,13 +14,14 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -30,7 +31,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.mapNotNull
@ -38,12 +38,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs 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.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.browser.thumbnails.BrowserThumbnails
@ -69,6 +71,7 @@ import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.PermissionsFeature
@ -76,6 +79,7 @@ import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard 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 mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -85,7 +89,6 @@ import org.mozilla.fenix.OnBackLongPressedListener
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider import org.mozilla.fenix.components.StoreProvider
@ -127,19 +130,19 @@ import java.lang.ref.WeakReference
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer, abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator private lateinit var browserAnimator: BrowserAnimator
private lateinit var components: Components
private var _browserInteractor: BrowserToolbarViewInteractor? = null private var _browserInteractor: BrowserToolbarViewInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor protected val browserInteractor: BrowserToolbarViewInteractor
get() = _browserInteractor!! get() = _browserInteractor!!
private var _browserToolbarView: BrowserToolbarView? = null private var _browserToolbarView: BrowserToolbarView? = null
protected val browserToolbarView: BrowserToolbarView @VisibleForTesting
internal val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!! get() = _browserToolbarView!!
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>() protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
@ -166,7 +169,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
var customTabSessionId: String? = null var customTabSessionId: String? = null
private var browserInitialized: Boolean = false @VisibleForTesting
internal var browserInitialized: Boolean = false
private var initUIJob: Job? = null private var initUIJob: Job? = null
protected var webAppToolbarShouldBeVisible = true protected var webAppToolbarShouldBeVisible = true
@ -192,28 +196,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
val view = inflater.inflate(R.layout.fragment_browser, container, false) val view = inflater.inflate(R.layout.fragment_browser, container, false)
val activity = activity as HomeActivity val activity = activity as HomeActivity
components = requireComponents
if (customTabSessionId == null) {
// Once tab restoration is complete, if there are no tabs to show in the browser, go home
components.core.store.flowScoped(viewLifecycleOwner) { flow ->
flow.map { state -> state.restoreComplete }
.ifChanged()
.collect { restored ->
if (restored) {
val tabs =
components.core.store.state.getNormalOrPrivateTabs(
activity.browsingModeManager.mode.isPrivate
)
if (tabs.isEmpty()) findNavController().popBackStack(
R.id.homeFragment,
false
)
}
}
}
}
activity.themeManager.applyStatusBarTheme(activity) activity.themeManager.applyStatusBarTheme(activity)
browserFragmentStore = StoreProvider.get(this) { browserFragmentStore = StoreProvider.get(this) {
@ -227,16 +209,24 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
browserInitialized = initializeUI(view) != null browserInitialized = initializeUI(view) != null
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
if (customTabSessionId == null) {
// We currently only need this observer to navigate to home
// in case all tabs have been removed on startup. No need to
// this if we have a known session to display.
observeRestoreComplete(requireComponents.core.store, findNavController())
} }
private val homeViewModel: HomeScreenViewModel by activityViewModels { observeTabSelection(requireComponents.core.store)
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652 requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
} }
private val homeViewModel: HomeScreenViewModel by activityViewModels()
@Suppress("ComplexMethod", "LongMethod") @Suppress("ComplexMethod", "LongMethod")
@CallSuper @CallSuper
protected open fun initializeUI(view: View): Session? { @VisibleForTesting
internal open fun initializeUI(view: View): Session? {
val context = requireContext() val context = requireContext()
val sessionManager = context.components.core.sessionManager val sessionManager = context.components.core.sessionManager
val store = context.components.core.store val store = context.components.core.store
@ -248,14 +238,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
fragment = WeakReference(this), fragment = WeakReference(this),
engineView = WeakReference(engineView), engineView = WeakReference(engineView),
swipeRefresh = WeakReference(swipeRefresh), swipeRefresh = WeakReference(swipeRefresh),
viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope), viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope)
settings = context.components.settings,
firstContentfulHappened = ::didFirstContentfulHappen
).apply { ).apply {
beginAnimateInIfNecessary() beginAnimateInIfNecessary()
} }
return getSessionById()?.also { session -> return getSessionById()?.also { _ ->
val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
putExtra(HomeActivity.OPEN_TO_BROWSER, true) putExtra(HomeActivity.OPEN_TO_BROWSER, true)
@ -316,10 +304,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
browserAnimator = browserAnimator, browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } }, bookmarkTapped = { url: String, title: String ->
viewLifecycleOwner.lifecycleScope.launch {
bookmarkTapped(url, title)
}
},
scope = viewLifecycleOwner.lifecycleScope, scope = viewLifecycleOwner.lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage, tabCollectionStorage = requireComponents.core.tabCollectionStorage,
topSitesStorage = requireComponents.core.topSitesStorage topSitesStorage = requireComponents.core.topSitesStorage,
browserStore = store
) )
_browserInteractor = BrowserInteractor( _browserInteractor = BrowserInteractor(
@ -582,7 +575,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
feature = SitePermissionsFeature( feature = SitePermissionsFeature(
context = context, context = context,
storage = context.components.core.permissionStorage.permissionsStorage, storage = context.components.core.permissionStorage.permissionsStorage,
sessionManager = sessionManager,
fragmentManager = parentFragmentManager, fragmentManager = parentFragmentManager,
promptsStyling = SitePermissionsFeature.PromptsStyling( promptsStyling = SitePermissionsFeature.PromptsStyling(
gravity = getAppropriateLayoutGravity(), gravity = getAppropriateLayoutGravity(),
@ -598,7 +590,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
shouldShowRequestPermissionRationale( shouldShowRequestPermissionRationale(
it it
) )
}), },
store = store
),
owner = this, owner = this,
view = view view = view
) )
@ -631,28 +625,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
view = view view = view
) )
session.register(observer = object : Session.Observer { expandToolbarOnNavigation(store)
override fun onUrlChanged(session: Session, url: String) {
browserToolbarView.expand()
}
override fun onLoadRequest(
session: Session,
url: String,
triggeredByRedirect: Boolean,
triggeredByWebContent: Boolean
) {
browserToolbarView.expand()
}
}, owner = viewLifecycleOwner)
sessionManager.register(observer = object : SessionManager.Observer {
override fun onSessionSelected(session: Session) {
fullScreenChanged(false)
browserToolbarView.expand()
resumeDownloadDialogState(session.id, store, view, context, toolbarHeight)
}
}, owner = viewLifecycleOwner)
store.flowScoped(viewLifecycleOwner) { flow -> store.flowScoped(viewLifecycleOwner) { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) } flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) }
@ -660,28 +633,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
.collect { tab -> pipModeChanged(tab) } .collect { tab -> pipModeChanged(tab) }
} }
if (context.settings().waitToShowPageUntilFirstPaint) {
store.flowScoped(viewLifecycleOwner) { flow ->
flow.mapNotNull { state ->
state.findTabOrCustomTabOrSelectedTab(
customTabSessionId
)
}
.ifChanged { it.content.firstContentfulPaint }
.collect {
val showEngineView =
it.content.firstContentfulPaint || it.content.progress == LOADING_PROGRESS_COMPLETE
if (showEngineView) {
engineView?.asView()?.isVisible = true
swipeRefresh?.alpha = 1f
} else {
engineView?.asView()?.isVisible = false
}
}
}
}
view.swipeRefresh.isEnabled = view.swipeRefresh.isEnabled =
FeatureFlags.pullToRefreshEnabled && context.settings().isPullToRefreshEnabledInBrowser FeatureFlags.pullToRefreshEnabled && context.settings().isPullToRefreshEnabledInBrowser
if (view.swipeRefresh.isEnabled) { if (view.swipeRefresh.isEnabled) {
@ -718,6 +669,21 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
} }
@VisibleForTesting
internal fun expandToolbarOnNavigation(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.mapNotNull {
state -> state.findCustomTabOrSelectedTab(customTabSessionId)
}
.ifAnyChanged {
tab -> arrayOf(tab.content.url, tab.content.loadRequest)
}
.collect {
browserToolbarView.expand()
}
}
}
/** /**
* Preserves current state of the [DynamicDownloadDialog] to persist through tab changes and * Preserves current state of the [DynamicDownloadDialog] to persist through tab changes and
* other fragments navigation. * other fragments navigation.
@ -741,7 +707,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* onTryAgain it will use [ContentAction.UpdateDownloadAction] to re-enqueue the former failed * onTryAgain it will use [ContentAction.UpdateDownloadAction] to re-enqueue the former failed
* download, because [DownloadsFeature] clears any queued downloads onStop. * download, because [DownloadsFeature] clears any queued downloads onStop.
* */ * */
private fun resumeDownloadDialogState( @VisibleForTesting
internal fun resumeDownloadDialogState(
sessionId: String?, sessionId: String?,
store: BrowserStore, store: BrowserStore,
view: View, view: View,
@ -834,26 +801,66 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
): List<ContextMenuCandidate> ): List<ContextMenuCandidate>
@CallSuper @CallSuper
override fun onSessionSelected(session: Session) { override fun onStart() {
if (!this.isRemoving) { super.onStart()
updateThemeForSession(session) sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
}
@VisibleForTesting
internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) {
val activity = activity as HomeActivity
consumeFlow(store) { flow ->
flow.map { state -> state.restoreComplete }
.ifChanged()
.collect { restored ->
if (restored) {
// Once tab restoration is complete, if there are no tabs to show in the browser, go home
val tabs =
store.state.getNormalOrPrivateTabs(
activity.browsingModeManager.mode.isPrivate
)
if (tabs.isEmpty() || store.state.selectedTabId == null) {
navController.popBackStack(R.id.homeFragment, false)
} }
if (!browserInitialized) {
// Initializing a new coroutineScope to avoid ConcurrentModificationException in ObserverRegistry
// This will be removed when ObserverRegistry is deprecated by browser-state.
initUIJob = MainScope().launch {
view?.let {
browserInitialized = initializeUI(it) != null
} }
} }
} }
} }
@CallSuper @VisibleForTesting
override fun onStart() { internal fun observeTabSelection(store: BrowserStore) {
super.onStart() consumeFlow(store) { flow ->
requireComponents.core.sessionManager.register(this, this, autoPause = true) flow.ifChanged {
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener() it.selectedTabId
}
.mapNotNull {
it.selectedTab
}
.collect {
handleTabSelected(it)
}
}
}
private fun handleTabSelected(selectedTab: TabSessionState) {
if (!this.isRemoving) {
updateThemeForSession(selectedTab)
}
if (browserInitialized) {
view?.let { view ->
fullScreenChanged(false)
browserToolbarView.expand()
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
val context = requireContext()
resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight)
}
} else {
view?.let { view ->
browserInitialized = initializeUI(view) != null
}
}
} }
@CallSuper @CallSuper
@ -868,7 +875,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
hideToolbar() hideToolbar()
getSessionById()?.let { updateThemeForSession(it) } components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)?.let {
updateThemeForSession(it)
}
} }
@CallSuper @CallSuper
@ -992,13 +1001,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog. * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
*/ */
protected fun getAppropriateLayoutGravity(): Int = protected fun getAppropriateLayoutGravity(): Int =
components.settings.toolbarPosition.androidGravity requireComponents.settings.toolbarPosition.androidGravity
/** /**
* Updates the site permissions rules based on user settings. * Updates the site permissions rules based on user settings.
*/ */
private fun assignSitePermissionsRules() { private fun assignSitePermissionsRules() {
val rules = components.settings.getSitePermissionsCustomSettingsRules() val rules = requireComponents.settings.getSitePermissionsCustomSettingsRules()
sitePermissionsFeature.withFeature { sitePermissionsFeature.withFeature {
it.sitePermissionsRules = rules it.sitePermissionsRules = rules
@ -1033,8 +1042,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
/** /**
* Set the activity normal/private theme to match the current session. * Set the activity normal/private theme to match the current session.
*/ */
private fun updateThemeForSession(session: Session) { @VisibleForTesting
val sessionMode = BrowsingMode.fromBoolean(session.private) internal fun updateThemeForSession(session: SessionState) {
val sessionMode = BrowsingMode.fromBoolean(session.content.private)
(activity as HomeActivity).browsingModeManager.mode = sessionMode (activity as HomeActivity).browsingModeManager.mode = sessionMode
} }
@ -1042,7 +1052,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* Returns the current session. * Returns the current session.
*/ */
protected fun getSessionById(): Session? { protected fun getSessionById(): Session? {
val sessionManager = components.core.sessionManager val sessionManager = requireComponents.core.sessionManager
val localCustomTabId = customTabSessionId val localCustomTabId = customTabSessionId
return if (localCustomTabId != null) { return if (localCustomTabId != null) {
sessionManager.findSessionById(localCustomTabId) sessionManager.findSessionById(localCustomTabId)
@ -1051,10 +1061,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
} }
private suspend fun bookmarkTapped(session: Session) = withContext(IO) { private suspend fun bookmarkTapped(sessionUrl: String, sessionTitle: String) = withContext(IO) {
val bookmarksStorage = requireComponents.core.bookmarksStorage val bookmarksStorage = requireComponents.core.bookmarksStorage
val existing = val existing =
bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url } bookmarksStorage.getBookmarksWithUrl(sessionUrl).firstOrNull { it.url == sessionUrl }
if (existing != null) { if (existing != null) {
// Bookmark exists, go to edit fragment // Bookmark exists, go to edit fragment
withContext(Main) { withContext(Main) {
@ -1067,8 +1077,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
// Save bookmark, then go to edit fragment // Save bookmark, then go to edit fragment
val guid = bookmarksStorage.addItem( val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id, BookmarkRoot.Mobile.id,
url = session.url, url = sessionUrl,
title = session.title, title = sessionTitle,
position = null position = null
) )
@ -1121,7 +1131,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
} }
private fun fullScreenChanged(inFullScreen: Boolean) { @VisibleForTesting
internal fun fullScreenChanged(inFullScreen: Boolean) {
if (inFullScreen) { if (inFullScreen) {
// Close find in page bar if opened // Close find in page bar if opened
findInPageIntegration.onBackPressed() findInPageIntegration.onBackPressed()
@ -1154,15 +1165,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
} }
} }
private fun didFirstContentfulHappen() =
if (components.settings.waitToShowPageUntilFirstPaint) {
val tab =
components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)
tab?.content?.firstContentfulPaint ?: false
} else {
true
}
/* /*
* Dereference these views when the fragment view is destroyed to prevent memory leaks * Dereference these views when the fragment view is destroyed to prevent memory leaks
*/ */
@ -1205,8 +1207,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val REQUEST_CODE_APP_PERMISSIONS = 3 private const val REQUEST_CODE_APP_PERMISSIONS = 3
private const val LOADING_PROGRESS_COMPLETE = 100
} }
override fun onAccessibilityStateChanged(enabled: Boolean) { override fun onAccessibilityStateChanged(enabled: Boolean) {

@ -12,13 +12,11 @@ import androidx.core.graphics.drawable.toDrawable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.utils.Settings
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
/** /**
@ -29,9 +27,7 @@ class BrowserAnimator(
private val fragment: WeakReference<Fragment>, private val fragment: WeakReference<Fragment>,
private val engineView: WeakReference<EngineView>, private val engineView: WeakReference<EngineView>,
private val swipeRefresh: WeakReference<View>, private val swipeRefresh: WeakReference<View>,
private val viewLifecycleScope: WeakReference<LifecycleCoroutineScope>, private val viewLifecycleScope: WeakReference<LifecycleCoroutineScope>
private val settings: Settings,
private val firstContentfulHappened: () -> Boolean
) { ) {
private val unwrappedEngineView: EngineView? private val unwrappedEngineView: EngineView?
@ -41,20 +37,8 @@ class BrowserAnimator(
get() = swipeRefresh.get() get() = swipeRefresh.get()
fun beginAnimateInIfNecessary() { fun beginAnimateInIfNecessary() {
if (settings.waitToShowPageUntilFirstPaint) {
if (firstContentfulHappened()) {
viewLifecycleScope.get()?.launch {
delay(ANIMATION_DELAY)
unwrappedEngineView?.asView()?.visibility = View.VISIBLE unwrappedEngineView?.asView()?.visibility = View.VISIBLE
unwrappedSwipeRefresh?.background = null unwrappedSwipeRefresh?.background = null
unwrappedSwipeRefresh?.alpha = 1f
}
}
} else {
unwrappedSwipeRefresh?.alpha = 1f
unwrappedEngineView?.asView()?.visibility = View.VISIBLE
unwrappedSwipeRefresh?.background = null
}
} }
/** /**
@ -93,8 +77,6 @@ class BrowserAnimator(
} }
companion object { companion object {
private const val ANIMATION_DELAY = 100L
fun getToolbarNavOptions(context: Context): NavOptions { fun getToolbarNavOptions(context: Context): NavOptions {
val navOptions = NavOptions.Builder() val navOptions = NavOptions.Builder()

@ -164,7 +164,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
navController = findNavController(), navController = findNavController(),
settings = settings, settings = settings,
appLinksUseCases = context.components.useCases.appLinksUseCases, appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserToolbarView.view.parent as ViewGroup container = browserLayout as ViewGroup
) )
session.register( session.register(
openInAppOnboardingObserver!!, openInAppOnboardingObserver!!,

@ -10,7 +10,7 @@ import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.res.ResourcesCompat import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.doOnNextLayout import androidx.core.view.doOnNextLayout
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.* import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
@ -40,10 +40,9 @@ class TabPreview @JvmOverloads constructor(
gravity = Gravity.TOP gravity = Gravity.TOP
} }
fakeToolbar.background = ResourcesCompat.getDrawable( fakeToolbar.background = AppCompatResources.getDrawable(
resources, context,
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context), ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context)
null
) )
} }
@ -64,7 +63,10 @@ class TabPreview @JvmOverloads constructor(
fun loadPreviewThumbnail(thumbnailId: String) { fun loadPreviewThumbnail(thumbnailId: String) {
doOnNextLayout { doOnNextLayout {
val thumbnailSize = max(previewThumbnail.height, previewThumbnail.width) val thumbnailSize = max(previewThumbnail.height, previewThumbnail.width)
thumbnailLoader.loadIntoView(previewThumbnail, ImageLoadRequest(thumbnailId, thumbnailSize)) thumbnailLoader.loadIntoView(
previewThumbnail,
ImageLoadRequest(thumbnailId, thumbnailSize)
)
} }
} }
} }

@ -156,14 +156,14 @@ class ToolbarGestureHandler(
val sessions = sessionManager.sessionsOfType(currentSession.private) val sessions = sessionManager.sessionsOfType(currentSession.private)
val index = when (gestureDirection) { val index = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) { GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
currentIndex - 1
} else {
currentIndex + 1 currentIndex + 1
} else {
currentIndex - 1
} }
GestureDirection.LEFT_TO_RIGHT -> if (isLtr) { GestureDirection.LEFT_TO_RIGHT -> if (isLtr) {
currentIndex + 1
} else {
currentIndex - 1 currentIndex - 1
} else {
currentIndex + 1
} }
} }

@ -1,107 +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.cfr
import android.app.Dialog
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import androidx.core.view.isVisible
import androidx.core.view.marginTop
import kotlinx.android.synthetic.main.search_widget_cfr.view.*
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.drop_down_triangle
import kotlinx.android.synthetic.main.tracking_protection_onboarding_popup.view.pop_up_triangle
import org.mozilla.fenix.R
import org.mozilla.fenix.components.SearchWidgetCreator
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.utils.Settings
/**
* Displays a CFR above the HomeFragment toolbar that recommends usage / installation of the search widget.
*/
class SearchWidgetCFR(
private val context: Context,
private val settings: Settings,
private val metrics: MetricController,
private val getToolbar: () -> View
) {
fun displayIfNecessary() {
if (settings.isInSearchWidgetExperiment &&
settings.shouldDisplaySearchWidgetCfr() &&
!isShown
) {
isShown = true
showSearchWidgetCFR()
}
}
@Suppress("InflateParams")
private fun showSearchWidgetCFR() {
settings.lastCfrShownTimeInMillis = System.currentTimeMillis()
settings.incrementSearchWidgetCFRDisplayed()
val searchWidgetCFRDialog = Dialog(context)
val layout = LayoutInflater.from(context)
.inflate(R.layout.search_widget_cfr, null)
val toolbarPosition = settings.toolbarPosition
layout.drop_down_triangle.isVisible = toolbarPosition == ToolbarPosition.TOP
layout.pop_up_triangle.isVisible = toolbarPosition == ToolbarPosition.BOTTOM
val toolbar = getToolbar()
val gravity = Gravity.CENTER_HORIZONTAL or toolbarPosition.androidGravity
layout.cfr_neg_button.setOnClickListener {
metrics.track(Event.SearchWidgetCFRNotNowPressed)
searchWidgetCFRDialog.dismiss()
settings.manuallyDismissSearchWidgetCFR()
}
layout.cfr_pos_button.setOnClickListener {
metrics.track(Event.SearchWidgetCFRAddWidgetPressed)
SearchWidgetCreator.createSearchWidget(context)
searchWidgetCFRDialog.dismiss()
settings.manuallyDismissSearchWidgetCFR()
}
searchWidgetCFRDialog.apply {
setContentView(layout)
}
searchWidgetCFRDialog.window?.let {
it.setGravity(gravity)
val attr = it.attributes
attr.y =
(toolbar.y + toolbar.height - toolbar.marginTop - toolbar.paddingTop).toInt()
it.attributes = attr
it.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
searchWidgetCFRDialog.setOnCancelListener {
isShown = false
metrics.track(Event.SearchWidgetCFRCanceled)
}
searchWidgetCFRDialog.setOnDismissListener {
isShown = false
settings.incrementSearchWidgetCFRDismissed()
}
searchWidgetCFRDialog.show()
metrics.track(Event.SearchWidgetCFRDisplayed)
}
companion object {
// Used to ensure multiple dialogs are not shown on top of each other
var isShown = false
}
}

@ -16,7 +16,7 @@ import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
/** /**

@ -23,6 +23,7 @@ import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.LeanplumMetricsService import org.mozilla.fenix.components.metrics.LeanplumMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
@ -36,7 +37,7 @@ import org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL
class Analytics( class Analytics(
private val context: Context private val context: Context
) { ) {
val crashReporter: CrashReporter by lazy { val crashReporter: CrashReporter by lazyMonitored {
val services = mutableListOf<CrashReporterService>() val services = mutableListOf<CrashReporterService>()
if (isSentryEnabled()) { if (isSentryEnabled()) {
@ -84,9 +85,9 @@ class Analytics(
) )
} }
val leanplumMetricsService by lazy { LeanplumMetricsService(context as Application) } val leanplumMetricsService by lazyMonitored { LeanplumMetricsService(context as Application) }
val metrics: MetricController by lazy { val metrics: MetricController by lazyMonitored {
MetricController.create( MetricController.create(
listOf( listOf(
GleanMetricsService(context), GleanMetricsService(context),

@ -36,11 +36,12 @@ import mozilla.components.service.sync.logins.SyncableLoginsStorage
import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.sync.SyncedTabsIntegration import org.mozilla.fenix.sync.SyncedTabsIntegration
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -109,9 +110,11 @@ class BackgroundServices(
val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode) val accountAbnormalities = AccountAbnormalities(context, crashReporter, strictMode)
val accountManager by lazy { makeAccountManager(context, serverConfig, deviceConfig, syncConfig) } val accountManager by lazyMonitored {
makeAccountManager(context, serverConfig, deviceConfig, syncConfig, crashReporter)
}
val syncedTabsStorage by lazy { val syncedTabsStorage by lazyMonitored {
SyncedTabsStorage(accountManager, context.components.core.store, remoteTabsStorage.value) SyncedTabsStorage(accountManager, context.components.core.store, remoteTabsStorage.value)
} }
@ -120,7 +123,8 @@ class BackgroundServices(
context: Context, context: Context,
serverConfig: ServerConfig, serverConfig: ServerConfig,
deviceConfig: DeviceConfig, deviceConfig: DeviceConfig,
syncConfig: SyncConfig? syncConfig: SyncConfig?,
crashReporter: CrashReporter?
) = FxaAccountManager( ) = FxaAccountManager(
context, context,
serverConfig, serverConfig,
@ -136,7 +140,8 @@ class BackgroundServices(
// Necessary to enable "Manage Account" functionality and ability to generate OAuth // Necessary to enable "Manage Account" functionality and ability to generate OAuth
// codes for certain scopes. // codes for certain scopes.
SCOPE_SESSION SCOPE_SESSION
) ),
crashReporter
).also { accountManager -> ).also { accountManager ->
// TODO this needs to change once we have a SyncManager // TODO this needs to change once we have a SyncManager
context.settings().fxaHasSyncedItems = accountManager.authenticatedAccount()?.let { context.settings().fxaHasSyncedItems = accountManager.authenticatedAccount()?.let {
@ -152,7 +157,7 @@ class BackgroundServices(
// Enable push if it's configured. // Enable push if it's configured.
push.feature?.let { autoPushFeature -> push.feature?.let { autoPushFeature ->
FxaPushSupportFeature(context, accountManager, autoPushFeature) FxaPushSupportFeature(context, accountManager, autoPushFeature, crashReporter)
} }
SendTabFeature(accountManager) { device, tabs -> SendTabFeature(accountManager) { device, tabs ->
@ -172,7 +177,7 @@ class BackgroundServices(
/** /**
* Provides notification functionality, manages notification channels. * Provides notification functionality, manages notification channels.
*/ */
private val notificationManager by lazy { private val notificationManager by lazyMonitored {
NotificationManager(context) NotificationManager(context)
} }
} }

@ -18,9 +18,10 @@ import mozilla.components.support.migration.state.MigrationStore
import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -38,7 +39,7 @@ private const val DAY_IN_MINUTES = 24 * 60L
*/ */
@Mockable @Mockable
class Components(private val context: Context) { class Components(private val context: Context) {
val backgroundServices by lazy { val backgroundServices by lazyMonitored {
BackgroundServices( BackgroundServices(
context, context,
push, push,
@ -50,10 +51,10 @@ class Components(private val context: Context) {
strictMode strictMode
) )
} }
val services by lazy { Services(context, backgroundServices.accountManager) } val services by lazyMonitored { Services(context, backgroundServices.accountManager) }
val core by lazy { Core(context, analytics.crashReporter, strictMode) } val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) }
val search by lazy { Search(context) } val search by lazyMonitored { Search(context) }
val useCases by lazy { val useCases by lazyMonitored {
UseCases( UseCases(
context, context,
core.engine, core.engine,
@ -64,7 +65,7 @@ class Components(private val context: Context) {
core.topSitesStorage core.topSitesStorage
) )
} }
val intentProcessors by lazy { val intentProcessors by lazyMonitored {
IntentProcessors( IntentProcessors(
context, context,
core.sessionManager, core.sessionManager,
@ -77,27 +78,37 @@ class Components(private val context: Context) {
) )
} }
val addonCollectionProvider by lazy { val addonCollectionProvider by lazyMonitored {
val addonsAccount = context.settings().customAddonsAccount // Check if we have a customized (overridden) AMO collection (only supported in Nightly)
val addonsCollection = context.settings().customAddonsCollection if (Config.channel.isNightlyOrDebug && context.settings().amoCollectionOverrideConfigured()) {
PagedAddonCollectionProvider(
context,
core.client,
collectionAccount = context.settings().overrideAmoUser,
collectionName = context.settings().overrideAmoCollection
)
}
// Use Iceraven settings config otherwise
else {
PagedAddonCollectionProvider( PagedAddonCollectionProvider(
context, context,
core.client, core.client,
collectionAccount = addonsAccount, collectionAccount = context.settings().customAddonsAccount,
collectionName = addonsCollection, collectionName = context.settings().customAddonsCollection,
maxCacheAgeInMinutes = DAY_IN_MINUTES maxCacheAgeInMinutes = DAY_IN_MINUTES
) )
} }
}
val appStartupTelemetry by lazy { AppStartupTelemetry(analytics.metrics) } val appStartupTelemetry by lazyMonitored { AppStartupTelemetry(analytics.metrics) }
@Suppress("MagicNumber") @Suppress("MagicNumber")
val addonUpdater by lazy { val addonUpdater by lazyMonitored {
DefaultAddonUpdater(context, AddonUpdater.Frequency(12, TimeUnit.HOURS)) DefaultAddonUpdater(context, AddonUpdater.Frequency(12, TimeUnit.HOURS))
} }
@Suppress("MagicNumber") @Suppress("MagicNumber")
val supportedAddonsChecker by lazy { val supportedAddonsChecker by lazyMonitored {
DefaultSupportedAddonsChecker(context, SupportedAddonsChecker.Frequency(12, TimeUnit.HOURS), DefaultSupportedAddonsChecker(context, SupportedAddonsChecker.Frequency(12, TimeUnit.HOURS),
onNotificationClickIntent = Intent(context, HomeActivity::class.java).apply { onNotificationClickIntent = Intent(context, HomeActivity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
@ -107,7 +118,7 @@ class Components(private val context: Context) {
) )
} }
val addonManager by lazy { val addonManager by lazyMonitored {
AddonManager(core.store, core.engine, addonCollectionProvider, addonUpdater) AddonManager(core.store, core.engine, addonCollectionProvider, addonUpdater)
} }
@ -120,18 +131,18 @@ class Components(private val context: Context) {
addonCollectionProvider.setCollectionName(addonsCollection) addonCollectionProvider.setCollectionName(addonsCollection)
} }
val analytics by lazy { Analytics(context) } val analytics by lazyMonitored { Analytics(context) }
val publicSuffixList by lazy { PublicSuffixList(context) } val publicSuffixList by lazyMonitored { PublicSuffixList(context) }
val clipboardHandler by lazy { ClipboardHandler(context) } val clipboardHandler by lazyMonitored { ClipboardHandler(context) }
val migrationStore by lazy { MigrationStore() } val migrationStore by lazyMonitored { MigrationStore() }
val performance by lazy { PerformanceComponent() } val performance by lazyMonitored { PerformanceComponent() }
val push by lazy { Push(context, analytics.crashReporter) } val push by lazyMonitored { Push(context, analytics.crashReporter) }
val wifiConnectionMonitor by lazy { WifiConnectionMonitor(context as Application) } val wifiConnectionMonitor by lazyMonitored { WifiConnectionMonitor(context as Application) }
val strictMode by lazy { StrictModeManager(Config, this) } val strictMode by lazyMonitored { StrictModeManager(Config, this) }
val settings by lazy { Settings(context) } val settings by lazyMonitored { Settings(context) }
val reviewPromptController by lazy { val reviewPromptController by lazyMonitored {
ReviewPromptController( ReviewPromptController(
context, context,
FenixReviewSettings(settings) FenixReviewSettings(settings)

@ -9,6 +9,7 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.StrictMode import android.os.StrictMode
import androidx.core.content.ContextCompat
import io.sentry.Sentry import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -22,8 +23,8 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.session.undo.UndoMiddleware import mozilla.components.browser.session.undo.UndoMiddleware
import mozilla.components.browser.state.action.RestoreCompleteAction
import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.action.RestoreCompleteAction
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
@ -62,11 +63,12 @@ import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.TelemetryMiddleware import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.media.MediaService import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
@ -90,7 +92,7 @@ class Core(
* The browser engine component initialized based on the build * The browser engine component initialized based on the build
* configuration (see build variants). * configuration (see build variants).
*/ */
val engine: Engine by lazy { val engine: Engine by lazyMonitored {
val defaultSettings = DefaultSettings( val defaultSettings = DefaultSettings(
requestInterceptor = AppRequestInterceptor(context), requestInterceptor = AppRequestInterceptor(context),
remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled && remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
@ -103,7 +105,11 @@ class Core(
fontInflationEnabled = context.settings().shouldUseAutoSize, fontInflationEnabled = context.settings().shouldUseAutoSize,
suspendMediaWhenInactive = false, suspendMediaWhenInactive = false,
forceUserScalableContent = context.settings().forceEnableZoom, forceUserScalableContent = context.settings().forceEnableZoom,
loginAutofillEnabled = context.settings().shouldAutofillLogins loginAutofillEnabled = context.settings().shouldAutofillLogins,
clearColor = ContextCompat.getColor(
context,
R.color.foundation_normal_theme
)
) )
GeckoEngine( GeckoEngine(
@ -132,7 +138,7 @@ class Core(
/** /**
* [Client] implementation to be used for code depending on `concept-fetch`` * [Client] implementation to be used for code depending on `concept-fetch``
*/ */
val client: Client by lazy { val client: Client by lazyMonitored {
GeckoViewFetchClient( GeckoViewFetchClient(
context, context,
GeckoProvider.getOrCreateRuntime( GeckoProvider.getOrCreateRuntime(
@ -143,14 +149,14 @@ class Core(
) )
} }
private val sessionStorage: SessionStorage by lazy { private val sessionStorage: SessionStorage by lazyMonitored {
SessionStorage(context, engine = engine) SessionStorage(context, engine = engine)
} }
/** /**
* The [BrowserStore] holds the global [BrowserState]. * The [BrowserStore] holds the global [BrowserState].
*/ */
val store by lazy { val store by lazyMonitored {
BrowserStore( BrowserStore(
middleware = listOf( middleware = listOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine), RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
@ -181,12 +187,12 @@ class Core(
/** /**
* The [CustomTabsServiceStore] holds global custom tabs related data. * The [CustomTabsServiceStore] holds global custom tabs related data.
*/ */
val customTabsStore by lazy { CustomTabsServiceStore() } val customTabsStore by lazyMonitored { CustomTabsServiceStore() }
/** /**
* The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities. * The [RelationChecker] checks Digital Asset Links relationships for Trusted Web Activities.
*/ */
val relationChecker: RelationChecker by lazy { val relationChecker: RelationChecker by lazyMonitored {
StatementRelationChecker(StatementApi(client)) StatementRelationChecker(StatementApi(client))
} }
@ -196,7 +202,7 @@ class Core(
* sessions from the [SessionStorage], and with a default session (about:blank) in * sessions from the [SessionStorage], and with a default session (about:blank) in
* case all sessions/tabs are closed. * case all sessions/tabs are closed.
*/ */
val sessionManager by lazy { val sessionManager by lazyMonitored {
SessionManager(engine, store).also { sessionManager -> SessionManager(engine, store).also { sessionManager ->
// Install the "icons" WebExtension to automatically load icons for every visited website. // Install the "icons" WebExtension to automatically load icons for every visited website.
icons.install(engine, store) icons.install(engine, store)
@ -232,7 +238,8 @@ class Core(
// Now that we have restored our previous state (if there's one) let's remove timed out tabs // Now that we have restored our previous state (if there's one) let's remove timed out tabs
if (!context.settings().manuallyCloseTabs) { if (!context.settings().manuallyCloseTabs) {
store.state.tabs.filter { store.state.tabs.filter {
(System.currentTimeMillis() - it.lastAccess) > context.settings().getTabTimeout() (System.currentTimeMillis() - it.lastAccess) > context.settings()
.getTabTimeout()
}.forEach { }.forEach {
val session = sessionManager.findSessionById(it.id) val session = sessionManager.findSessionById(it.id)
if (session != null) { if (session != null) {
@ -254,26 +261,26 @@ class Core(
/** /**
* Icons component for loading, caching and processing website icons. * Icons component for loading, caching and processing website icons.
*/ */
val icons by lazy { val icons by lazyMonitored {
BrowserIcons(context, client) BrowserIcons(context, client)
} }
val metrics by lazy { val metrics by lazyMonitored {
context.components.analytics.metrics context.components.analytics.metrics
} }
val adsTelemetry by lazy { val adsTelemetry by lazyMonitored {
AdsTelemetry(metrics) AdsTelemetry(metrics)
} }
val searchTelemetry by lazy { val searchTelemetry by lazyMonitored {
InContentTelemetry(metrics) InContentTelemetry(metrics)
} }
/** /**
* Shortcut component for managing shortcuts on the device home screen. * Shortcut component for managing shortcuts on the device home screen.
*/ */
val webAppShortcutManager by lazy { val webAppShortcutManager by lazyMonitored {
WebAppShortcutManager( WebAppShortcutManager(
context, context,
client, client,
@ -286,21 +293,21 @@ class Core(
// Use these for startup-path code, where we don't want to do any work that's not strictly necessary. // Use these for startup-path code, where we don't want to do any work that's not strictly necessary.
// For example, this is how the GeckoEngine delegates (history, logins) are configured. // For example, this is how the GeckoEngine delegates (history, logins) are configured.
// We can fully initialize GeckoEngine without initialized our storage. // We can fully initialize GeckoEngine without initialized our storage.
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context, crashReporter) } val lazyHistoryStorage = lazyMonitored { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) } val lazyBookmarksStorage = lazyMonitored { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) } val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, passwordsEncryptionKey) }
/** /**
* The storage component to sync and persist tabs in a Firefox Sync account. * The storage component to sync and persist tabs in a Firefox Sync account.
*/ */
val lazyRemoteTabsStorage = lazy { RemoteTabsStorage() } val lazyRemoteTabsStorage = lazyMonitored { RemoteTabsStorage() }
// For most other application code (non-startup), these wrappers are perfectly fine and more ergonomic. // For most other application code (non-startup), these wrappers are perfectly fine and more ergonomic.
val historyStorage by lazy { lazyHistoryStorage.value } val historyStorage: PlacesHistoryStorage get() = lazyHistoryStorage.value
val bookmarksStorage by lazy { lazyBookmarksStorage.value } val bookmarksStorage: PlacesBookmarksStorage get() = lazyBookmarksStorage.value
val passwordsStorage by lazy { lazyPasswordsStorage.value } val passwordsStorage: SyncableLoginsStorage get() = lazyPasswordsStorage.value
val tabCollectionStorage by lazy { val tabCollectionStorage by lazyMonitored {
TabCollectionStorage( TabCollectionStorage(
context, context,
sessionManager, sessionManager,
@ -311,15 +318,30 @@ class Core(
/** /**
* A storage component for persisting thumbnail images of tabs. * A storage component for persisting thumbnail images of tabs.
*/ */
val thumbnailStorage by lazy { ThumbnailStorage(context) } val thumbnailStorage by lazyMonitored { ThumbnailStorage(context) }
val pinnedSiteStorage by lazy { PinnedSiteStorage(context) } val pinnedSiteStorage by lazyMonitored { PinnedSiteStorage(context) }
val topSitesStorage by lazy { val topSitesStorage by lazyMonitored {
val defaultTopSites = mutableListOf<Pair<String, String>>() val defaultTopSites = mutableListOf<Pair<String, String>>()
strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
if (!context.settings().defaultTopSitesAdded) { if (!context.settings().defaultTopSitesAdded) {
if (Config.channel.isMozillaOnline) {
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_baidu),
SupportUtils.BAIDU_URL
)
)
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_jd),
SupportUtils.JD_URL
)
)
} else {
defaultTopSites.add( defaultTopSites.add(
Pair( Pair(
context.getString(R.string.default_top_site_google), context.getString(R.string.default_top_site_google),
@ -342,6 +364,7 @@ class Core(
SupportUtils.WIKIPEDIA_URL SupportUtils.WIKIPEDIA_URL
) )
) )
}
context.settings().defaultTopSitesAdded = true context.settings().defaultTopSitesAdded = true
} }
@ -354,11 +377,11 @@ class Core(
) )
} }
val permissionStorage by lazy { PermissionStorage(context) } val permissionStorage by lazyMonitored { PermissionStorage(context) }
val webAppManifestStorage by lazy { ManifestStorage(context) } val webAppManifestStorage by lazyMonitored { ManifestStorage(context) }
val loginExceptionStorage by lazy { LoginExceptionStorage(context) } val loginExceptionStorage by lazyMonitored { LoginExceptionStorage(context) }
/** /**
* Shared Preferences that encrypt/decrypt using Android KeyStore and lib-dataprotect for 23+ * Shared Preferences that encrypt/decrypt using Android KeyStore and lib-dataprotect for 23+
@ -372,7 +395,7 @@ class Core(
forceInsecure = !Config.channel.isNightlyOrDebug forceInsecure = !Config.channel.isNightlyOrDebug
) )
private val passwordsEncryptionKey by lazy { private val passwordsEncryptionKey by lazyMonitored {
getSecureAbove22Preferences().getString(PASSWORDS_KEY) getSecureAbove22Preferences().getString(PASSWORDS_KEY)
?: generateEncryptionKey(KEY_STRENGTH).also { ?: generateEncryptionKey(KEY_STRENGTH).also {
if (context.settings().passwordsEncryptionKeyGenerated && if (context.settings().passwordsEncryptionKeyGenerated &&

@ -4,6 +4,7 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.graphics.Color
import android.util.TypedValue import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -32,7 +33,7 @@ class FenixSnackbar private constructor(
) : BaseTransientBottomBar<FenixSnackbar>(parent, content, contentViewCallback) { ) : BaseTransientBottomBar<FenixSnackbar>(parent, content, contentViewCallback) {
init { init {
view.background = null view.setBackgroundColor(Color.TRANSPARENT)
setAppropriateBackground(isError) setAppropriateBackground(isError)
@ -111,7 +112,8 @@ class FenixSnackbar private constructor(
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val content = inflater.inflate(R.layout.fenix_snackbar, parent, false) val content = inflater.inflate(R.layout.fenix_snackbar, parent, false)
val durationOrAccessibleDuration = if (parent.context.settings().accessibilityServicesEnabled) { val durationOrAccessibleDuration =
if (parent.context.settings().accessibilityServicesEnabled) {
LENGTH_ACCESSIBLE LENGTH_ACCESSIBLE
} else { } else {
duration duration

@ -19,6 +19,7 @@ import mozilla.components.support.migration.MigrationIntentProcessor
import mozilla.components.support.migration.state.MigrationStore import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.customtabs.FennecWebAppIntentProcessor import org.mozilla.fenix.customtabs.FennecWebAppIntentProcessor
import org.mozilla.fenix.home.intent.FennecBookmarkShortcutsIntentProcessor import org.mozilla.fenix.home.intent.FennecBookmarkShortcutsIntentProcessor
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/** /**
@ -39,26 +40,26 @@ class IntentProcessors(
/** /**
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents.
*/ */
val intentProcessor by lazy { val intentProcessor by lazyMonitored {
TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = false) TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = false)
} }
/** /**
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs.
*/ */
val privateIntentProcessor by lazy { val privateIntentProcessor by lazyMonitored {
TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = true) TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = true)
} }
val customTabIntentProcessor by lazy { val customTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = false) CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = false)
} }
val privateCustomTabIntentProcessor by lazy { val privateCustomTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = true) CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = true)
} }
val externalAppIntentProcessors by lazy { val externalAppIntentProcessors by lazyMonitored {
listOf( listOf(
TrustedWebActivityIntentProcessor( TrustedWebActivityIntentProcessor(
sessionManager = sessionManager, sessionManager = sessionManager,
@ -72,11 +73,11 @@ class IntentProcessors(
) )
} }
val fennecPageShortcutIntentProcessor by lazy { val fennecPageShortcutIntentProcessor by lazyMonitored {
FennecBookmarkShortcutsIntentProcessor(sessionManager, sessionUseCases.loadUrl) FennecBookmarkShortcutsIntentProcessor(sessionManager, sessionUseCases.loadUrl)
} }
val migrationIntentProcessor by lazy { val migrationIntentProcessor by lazyMonitored {
MigrationIntentProcessor(migrationStore) MigrationIntentProcessor(migrationStore)
} }
} }

@ -6,10 +6,11 @@ package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.VisualCompletenessQueue import org.mozilla.fenix.perf.VisualCompletenessQueue
import org.mozilla.fenix.perf.lazyMonitored
/** /**
* Component group for all functionality related to performance. * Component group for all functionality related to performance.
*/ */
class PerformanceComponent { class PerformanceComponent {
val visualCompletenessQueue by lazy { VisualCompletenessQueue(RunWhenReadyQueue()) } val visualCompletenessQueue by lazyMonitored { VisualCompletenessQueue(RunWhenReadyQueue()) }
} }

@ -10,6 +10,7 @@ import mozilla.components.feature.push.PushConfig
import mozilla.components.lib.crash.CrashReporter import mozilla.components.lib.crash.CrashReporter
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.push.FirebasePushService import org.mozilla.fenix.push.FirebasePushService
/** /**
@ -17,7 +18,7 @@ import org.mozilla.fenix.push.FirebasePushService
* push messaging (e.g. WebPush, SendTab). * push messaging (e.g. WebPush, SendTab).
*/ */
class Push(context: Context, crashReporter: CrashReporter) { class Push(context: Context, crashReporter: CrashReporter) {
val feature by lazy { val feature by lazyMonitored {
pushConfig?.let { config -> pushConfig?.let { config ->
AutoPushFeature( AutoPushFeature(
context = context, context = context,
@ -28,13 +29,13 @@ class Push(context: Context, crashReporter: CrashReporter) {
} }
} }
private val pushConfig: PushConfig? by lazy { private val pushConfig: PushConfig? by lazyMonitored {
val logger = Logger("PushConfig") val logger = Logger("PushConfig")
val projectIdKey = context.getString(R.string.pref_key_push_project_id) val projectIdKey = context.getString(R.string.pref_key_push_project_id)
val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName) val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName)
if (resId == 0) { if (resId == 0) {
logger.warn("No firebase configuration found; cannot support push service.") logger.warn("No firebase configuration found; cannot support push service.")
return@lazy null return@lazyMonitored null
} }
logger.debug("Creating push configuration for autopush.") logger.debug("Creating push configuration for autopush.")
@ -42,5 +43,5 @@ class Push(context: Context, crashReporter: CrashReporter) {
PushConfig(projectId) PushConfig(projectId)
} }
private val pushService by lazy { FirebasePushService() } private val pushService by lazyMonitored { FirebasePushService() }
} }

@ -10,6 +10,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.search.SearchEngineManager import mozilla.components.browser.search.SearchEngineManager
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/** /**
@ -22,7 +23,7 @@ class Search(private val context: Context) {
/** /**
* This component provides access to a centralized registry of search engines. * This component provides access to a centralized registry of search engines.
*/ */
val searchEngineManager by lazy { val searchEngineManager by lazyMonitored {
SearchEngineManager( SearchEngineManager(
coroutineContext = IO, coroutineContext = IO,
providers = listOf(provider) providers = listOf(provider)

@ -14,6 +14,7 @@ import mozilla.components.feature.app.links.AppLinksInterceptor
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.getPreferenceKey
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
@ -25,7 +26,7 @@ class Services(
private val context: Context, private val context: Context,
private val accountManager: FxaAccountManager private val accountManager: FxaAccountManager
) { ) {
val accountsAuthFeature by lazy { val accountsAuthFeature by lazyMonitored {
FirefoxAccountsAuthFeature(accountManager, FxaServer.REDIRECT_URL) { context, authUrl -> FirefoxAccountsAuthFeature(accountManager, FxaServer.REDIRECT_URL) { context, authUrl ->
CoroutineScope(Dispatchers.Main).launch { CoroutineScope(Dispatchers.Main).launch {
val intent = SupportUtils.createAuthCustomTabIntent(context, authUrl) val intent = SupportUtils.createAuthCustomTabIntent(context, authUrl)
@ -34,7 +35,7 @@ class Services(
} }
} }
val appLinksInterceptor by lazy { val appLinksInterceptor by lazyMonitored {
AppLinksInterceptor( AppLinksInterceptor(
context, context,
interceptLinkClicks = true, interceptLinkClicks = true,

@ -19,7 +19,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage import mozilla.components.feature.tab.collections.TabCollectionStorage
import mozilla.components.support.base.observer.Observable import mozilla.components.support.base.observer.Observable
import mozilla.components.support.base.observer.ObserverRegistry import mozilla.components.support.base.observer.ObserverRegistry
import org.mozilla.fenix.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.CollectionViewHolder

@ -22,6 +22,7 @@ import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.feature.top.sites.TopSitesUseCases
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/** /**
@ -42,19 +43,18 @@ class UseCases(
/** /**
* Use cases that provide engine interactions for a given browser session. * Use cases that provide engine interactions for a given browser session.
*/ */
val sessionUseCases by lazy { SessionUseCases(store, sessionManager) } val sessionUseCases by lazyMonitored { SessionUseCases(store, sessionManager) }
/** /**
* Use cases that provide tab management. * Use cases that provide tab management.
*/ */
val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store, sessionManager) } val tabsUseCases: TabsUseCases by lazyMonitored { TabsUseCases(store, sessionManager) }
/** /**
* Use cases that provide search engine integration. * Use cases that provide search engine integration.
*/ */
val searchUseCases by lazy { val searchUseCases by lazyMonitored {
SearchUseCases( SearchUseCases(
context,
store, store,
searchEngineManager.toDefaultSearchEngineProvider(context), searchEngineManager.toDefaultSearchEngineProvider(context),
sessionManager sessionManager
@ -64,22 +64,22 @@ class UseCases(
/** /**
* Use cases that provide settings management. * Use cases that provide settings management.
*/ */
val settingsUseCases by lazy { SettingsUseCases(engine, store) } val settingsUseCases by lazyMonitored { SettingsUseCases(engine, store) }
val appLinksUseCases by lazy { AppLinksUseCases(context.applicationContext) } val appLinksUseCases by lazyMonitored { AppLinksUseCases(context.applicationContext) }
val webAppUseCases by lazy { val webAppUseCases by lazyMonitored {
WebAppUseCases(context, sessionManager, shortcutManager) WebAppUseCases(context, sessionManager, shortcutManager)
} }
val downloadUseCases by lazy { DownloadsUseCases(store) } val downloadUseCases by lazyMonitored { DownloadsUseCases(store) }
val contextMenuUseCases by lazy { ContextMenuUseCases(store) } val contextMenuUseCases by lazyMonitored { ContextMenuUseCases(store) }
val trackingProtectionUseCases by lazy { TrackingProtectionUseCases(store, engine) } val trackingProtectionUseCases by lazyMonitored { TrackingProtectionUseCases(store, engine) }
/** /**
* Use cases that provide top sites management. * Use cases that provide top sites management.
*/ */
val topSitesUseCase by lazy { TopSitesUseCases(topSitesStorage) } val topSitesUseCase by lazyMonitored { TopSitesUseCases(topSitesStorage) }
} }

@ -4,10 +4,10 @@
package org.mozilla.fenix.components.history package org.mozilla.fenix.components.history
import kotlinx.coroutines.runBlocking
import mozilla.components.concept.storage.HistoryStorage import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.concept.storage.VisitInfo import mozilla.components.concept.storage.VisitInfo
import mozilla.components.concept.storage.VisitType import mozilla.components.concept.storage.VisitType
import org.mozilla.fenix.perf.runBlockingIncrement
/** /**
* An Interface for providing a paginated list of [VisitInfo] * An Interface for providing a paginated list of [VisitInfo]
@ -32,7 +32,7 @@ fun HistoryStorage.createSynchronousPagedHistoryProvider(): PagedHistoryProvider
numberOfItems: Long, numberOfItems: Long,
onComplete: (List<VisitInfo>) -> Unit onComplete: (List<VisitInfo>) -> Unit
) { ) {
runBlocking { runBlockingIncrement {
val history = getVisitsPaginated( val history = getVisitsPaginated(
offset, offset,
numberOfItems, numberOfItems,

@ -12,7 +12,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import kotlinx.coroutines.runBlocking
import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.SafeIntent
import org.mozilla.fenix.components.metrics.Event.AppAllStartup import org.mozilla.fenix.components.metrics.Event.AppAllStartup
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Source
@ -25,6 +24,7 @@ import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.ERROR
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.HOT
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.COLD
import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM import org.mozilla.fenix.components.metrics.Event.AppAllStartup.Type.WARM
import org.mozilla.fenix.perf.runBlockingIncrement
import java.lang.reflect.Modifier.PRIVATE import java.lang.reflect.Modifier.PRIVATE
/** /**
@ -186,7 +186,7 @@ class AppStartupTelemetry(
* the application potentially closes. * the application potentially closes.
*/ */
fun onStop() { fun onStop() {
runBlocking { runBlockingIncrement {
recordMetric() recordMetric()
} }
} }

@ -52,10 +52,6 @@ sealed class Event {
object CustomTabsActionTapped : Event() object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event() object CustomTabsMenuOpened : Event()
object UriOpened : Event() object UriOpened : Event()
object QRScannerOpened : Event()
object QRScannerPromptDisplayed : Event()
object QRScannerNavigationAllowed : Event()
object QRScannerNavigationDenied : Event()
object SyncAuthOpened : Event() object SyncAuthOpened : Event()
object SyncAuthClosed : Event() object SyncAuthClosed : Event()
object SyncAuthSignUp : Event() object SyncAuthSignUp : Event()
@ -70,7 +66,6 @@ sealed class Event {
object SyncAuthFromSharedReuse : Event() object SyncAuthFromSharedReuse : Event()
object SyncAuthFromSharedCopy : Event() object SyncAuthFromSharedCopy : Event()
object SyncAccountOpened : Event() object SyncAccountOpened : Event()
object SyncAccountClosed : Event()
object SyncAccountSyncNow : Event() object SyncAccountSyncNow : Event()
object SendTab : Event() object SendTab : Event()
object SignInToSendTab : Event() object SignInToSendTab : Event()
@ -98,11 +93,8 @@ sealed class Event {
object FindInPageOpened : Event() object FindInPageOpened : Event()
object FindInPageClosed : Event() object FindInPageClosed : Event()
object FindInPageSearchCommitted : Event() object FindInPageSearchCommitted : Event()
object PrivateBrowsingGarbageIconTapped : Event()
object PrivateBrowsingSnackbarUndoTapped : Event() object PrivateBrowsingSnackbarUndoTapped : Event()
object PrivateBrowsingNotificationTapped : Event() object PrivateBrowsingNotificationTapped : Event()
object PrivateBrowsingNotificationOpenTapped : Event()
object PrivateBrowsingNotificationDeleteAndOpenTapped : Event()
object PrivateBrowsingCreateShortcut : Event() object PrivateBrowsingCreateShortcut : Event()
object PrivateBrowsingAddShortcutCFR : Event() object PrivateBrowsingAddShortcutCFR : Event()
object PrivateBrowsingCancelCFR : Event() object PrivateBrowsingCancelCFR : Event()
@ -152,16 +144,11 @@ sealed class Event {
object FennecToFenixMigrated : Event() object FennecToFenixMigrated : Event()
object AddonsOpenInSettings : Event() object AddonsOpenInSettings : Event()
object VoiceSearchTapped : Event() object VoiceSearchTapped : Event()
object SearchWidgetCFRDisplayed : Event()
object SearchWidgetCFRCanceled : Event()
object SearchWidgetCFRNotNowPressed : Event()
object SearchWidgetCFRAddWidgetPressed : Event()
object SearchWidgetInstalled : Event() object SearchWidgetInstalled : Event()
object OnboardingAutoSignIn : Event() object OnboardingAutoSignIn : Event()
object OnboardingManualSignIn : Event() object OnboardingManualSignIn : Event()
object OnboardingPrivacyNotice : Event() object OnboardingPrivacyNotice : Event()
object OnboardingPrivateBrowsing : Event() object OnboardingPrivateBrowsing : Event()
object OnboardingWhatsNew : Event()
object OnboardingFinish : Event() object OnboardingFinish : Event()
object ChangedToDefaultBrowser : Event() object ChangedToDefaultBrowser : Event()
@ -508,7 +495,8 @@ sealed class Event {
"mozac.feature.contextmenu.save_image" to "save_image", "mozac.feature.contextmenu.save_image" to "save_image",
"mozac.feature.contextmenu.share_link" to "share_link", "mozac.feature.contextmenu.share_link" to "share_link",
"mozac.feature.contextmenu.copy_link" to "copy_link", "mozac.feature.contextmenu.copy_link" to "copy_link",
"mozac.feature.contextmenu.copy_image_location" to "copy_image_location" "mozac.feature.contextmenu.copy_image_location" to "copy_image_location",
"mozac.feature.contextmenu.share_image" to "share_image"
) )
} }
} }

@ -9,6 +9,7 @@ import mozilla.components.service.fxa.manager.SyncEnginesStorage
import mozilla.components.service.glean.Glean import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.private.NoExtraKeys import mozilla.components.service.glean.private.NoExtraKeys
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.AboutPage import org.mozilla.fenix.GleanMetrics.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
@ -38,13 +39,11 @@ import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.QrScanner
import org.mozilla.fenix.GleanMetrics.ReaderMode import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.SearchSuggestions import org.mozilla.fenix.GleanMetrics.SearchSuggestions
import org.mozilla.fenix.GleanMetrics.SearchWidget import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SearchWidgetCfr
import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.Tab
@ -230,18 +229,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.UriOpened -> EventWrapper<NoExtraKeys>( is Event.UriOpened -> EventWrapper<NoExtraKeys>(
{ Events.totalUriCount.add(1) } { Events.totalUriCount.add(1) }
) )
is Event.QRScannerOpened -> EventWrapper<NoExtraKeys>(
{ QrScanner.opened.record(it) }
)
is Event.QRScannerPromptDisplayed -> EventWrapper<NoExtraKeys>(
{ QrScanner.promptDisplayed.record(it) }
)
is Event.QRScannerNavigationAllowed -> EventWrapper<NoExtraKeys>(
{ QrScanner.navigationAllowed.record(it) }
)
is Event.QRScannerNavigationDenied -> EventWrapper<NoExtraKeys>(
{ QrScanner.navigationDenied.record(it) }
)
is Event.ErrorPageVisited -> EventWrapper( is Event.ErrorPageVisited -> EventWrapper(
{ ErrorPage.visitedError.record(it) }, { ErrorPage.visitedError.record(it) },
{ ErrorPage.visitedErrorKeys.valueOf(it) } { ErrorPage.visitedErrorKeys.valueOf(it) }
@ -270,9 +257,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>( is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>(
{ SyncAuth.otherExternal.record(it) } { SyncAuth.otherExternal.record(it) }
) )
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> EventWrapper<NoExtraKeys>(
{ SyncAuth.autoLogin.record(it) }
)
is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>( is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>(
{ SyncAuth.recovered.record(it) } { SyncAuth.recovered.record(it) }
) )
@ -285,9 +269,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAccountOpened -> EventWrapper<NoExtraKeys>( is Event.SyncAccountOpened -> EventWrapper<NoExtraKeys>(
{ SyncAccount.opened.record(it) } { SyncAccount.opened.record(it) }
) )
is Event.SyncAccountClosed -> EventWrapper<NoExtraKeys>(
{ SyncAccount.closed.record(it) }
)
is Event.SyncAccountSyncNow -> EventWrapper<NoExtraKeys>( is Event.SyncAccountSyncNow -> EventWrapper<NoExtraKeys>(
{ SyncAccount.syncNow.record(it) } { SyncAccount.syncNow.record(it) }
) )
@ -376,21 +357,12 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SearchWidgetVoiceSearchPressed -> EventWrapper<NoExtraKeys>( is Event.SearchWidgetVoiceSearchPressed -> EventWrapper<NoExtraKeys>(
{ SearchWidget.voiceButton.record(it) } { SearchWidget.voiceButton.record(it) }
) )
is Event.PrivateBrowsingGarbageIconTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.garbageIcon.record(it) }
)
is Event.PrivateBrowsingSnackbarUndoTapped -> EventWrapper<NoExtraKeys>( is Event.PrivateBrowsingSnackbarUndoTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.snackbarUndo.record(it) } { PrivateBrowsingMode.snackbarUndo.record(it) }
) )
is Event.PrivateBrowsingNotificationTapped -> EventWrapper<NoExtraKeys>( is Event.PrivateBrowsingNotificationTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.notificationTapped.record(it) } { PrivateBrowsingMode.notificationTapped.record(it) }
) )
is Event.PrivateBrowsingNotificationOpenTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.notificationOpen.record(it) }
)
is Event.PrivateBrowsingNotificationDeleteAndOpenTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.notificationDelete.record(it) }
)
is Event.PrivateBrowsingCreateShortcut -> EventWrapper<NoExtraKeys>( is Event.PrivateBrowsingCreateShortcut -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingShortcut.createShortcut.record(it) } { PrivateBrowsingShortcut.createShortcut.record(it) }
) )
@ -579,25 +551,10 @@ private val Event.wrapper: EventWrapper<*>?
is Event.VoiceSearchTapped -> EventWrapper<NoExtraKeys>( is Event.VoiceSearchTapped -> EventWrapper<NoExtraKeys>(
{ VoiceSearch.tapped.record(it) } { VoiceSearch.tapped.record(it) }
) )
is Event.SearchWidgetCFRDisplayed -> EventWrapper<NoExtraKeys>(
{ SearchWidgetCfr.displayed.record(it) }
)
is Event.SearchWidgetCFRCanceled -> EventWrapper<NoExtraKeys>(
{ SearchWidgetCfr.canceled.record(it) }
)
is Event.SearchWidgetCFRNotNowPressed -> EventWrapper<NoExtraKeys>(
{ SearchWidgetCfr.notNowPressed.record(it) }
)
is Event.SearchWidgetCFRAddWidgetPressed -> EventWrapper<NoExtraKeys>(
{ SearchWidgetCfr.addWidgetPressed.record(it) }
)
is Event.TabCounterMenuItemTapped -> EventWrapper( is Event.TabCounterMenuItemTapped -> EventWrapper(
{ Events.tabCounterMenuAction.record(it) }, { Events.tabCounterMenuAction.record(it) },
{ Events.tabCounterMenuActionKeys.valueOf(it) } { Events.tabCounterMenuActionKeys.valueOf(it) }
) )
is Event.OnboardingWhatsNew -> EventWrapper<NoExtraKeys>(
{ Onboarding.whatsNew.record(it) }
)
is Event.OnboardingPrivateBrowsing -> EventWrapper<NoExtraKeys>( is Event.OnboardingPrivateBrowsing -> EventWrapper<NoExtraKeys>(
{ Onboarding.prefToggledPrivateBrowsing.record(it) } { Onboarding.prefToggledPrivateBrowsing.record(it) }
) )
@ -721,6 +678,7 @@ private val Event.wrapper: EventWrapper<*>?
is Event.AddonInstalled -> null is Event.AddonInstalled -> null
is Event.SearchWidgetInstalled -> null is Event.SearchWidgetInstalled -> null
is Event.ChangedToDefaultBrowser -> null is Event.ChangedToDefaultBrowser -> null
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> null
} }
class GleanMetricsService( class GleanMetricsService(
@ -767,6 +725,14 @@ class GleanMetricsService(
mozillaProductDetector.getMozillaBrowserDefault(context)?.also { mozillaProductDetector.getMozillaBrowserDefault(context)?.also {
defaultMozBrowser.set(it) defaultMozBrowser.set(it)
} }
distributionId.set(
when (Config.channel.isMozillaOnline) {
true -> "MozillaOnline"
false -> "Mozilla"
}
)
mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context)) mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context))
adjustCampaign.set(context.settings().adjustCampaignId) adjustCampaign.set(context.settings().adjustCampaignId)

@ -12,7 +12,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.search.SearchEngine import mozilla.components.browser.search.SearchEngine
import mozilla.components.browser.search.provider.AssetsSearchEngineProvider import mozilla.components.browser.search.provider.AssetsSearchEngineProvider
import mozilla.components.browser.search.provider.SearchEngineList import mozilla.components.browser.search.provider.SearchEngineList
@ -27,6 +26,7 @@ import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.runBlockingIncrement
import java.util.Locale import java.util.Locale
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions")
@ -141,7 +141,7 @@ open class FenixSearchEngineProvider(
* are readily available throughout the app. Includes all installed engines, both * are readily available throughout the app. Includes all installed engines, both
* default and custom * default and custom
*/ */
fun installedSearchEngines(context: Context): SearchEngineList = runBlocking { fun installedSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context) val installedIdentifiers = installedSearchEngineIdentifiers(context)
val defaultList = searchEngines.await() val defaultList = searchEngines.await()
@ -161,15 +161,15 @@ open class FenixSearchEngineProvider(
) )
} }
fun allSearchEngineIdentifiers() = runBlocking { fun allSearchEngineIdentifiers() = runBlockingIncrement {
loadedSearchEngines.await().list.map { it.identifier } loadedSearchEngines.await().list.map { it.identifier }
} }
fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlocking { fun uninstalledSearchEngines(context: Context): SearchEngineList = runBlockingIncrement {
val installedIdentifiers = installedSearchEngineIdentifiers(context) val installedIdentifiers = installedSearchEngineIdentifiers(context)
val engineList = loadedSearchEngines.await() val engineList = loadedSearchEngines.await()
return@runBlocking engineList.copy( return@runBlockingIncrement engineList.copy(
list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) } list = engineList.list.filterNot { installedIdentifiers.contains(it.identifier) }
) )
} }
@ -182,7 +182,7 @@ open class FenixSearchEngineProvider(
context: Context, context: Context,
searchEngine: SearchEngine, searchEngine: SearchEngine,
isCustom: Boolean = false isCustom: Boolean = false
) = runBlocking { ) = runBlockingIncrement {
if (isCustom) { if (isCustom) {
val searchUrl = searchEngine.getSearchTemplate() val searchUrl = searchEngine.getSearchTemplate()
CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl) CustomSearchEngineStore.addSearchEngine(context, searchEngine.name, searchUrl)
@ -201,7 +201,7 @@ open class FenixSearchEngineProvider(
context: Context, context: Context,
searchEngine: SearchEngine, searchEngine: SearchEngine,
isCustom: Boolean = false isCustom: Boolean = false
) = runBlocking { ) = runBlockingIncrement {
if (isCustom) { if (isCustom) {
CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier) CustomSearchEngineStore.removeSearchEngine(context, searchEngine.identifier)
reload() reload()

@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
@ -63,10 +65,11 @@ class DefaultBrowserToolbarMenuController(
private val swipeRefresh: SwipeRefreshLayout, private val swipeRefresh: SwipeRefreshLayout,
private val customTabSession: Session?, private val customTabSession: Session?,
private val openInFenixIntent: Intent, private val openInFenixIntent: Intent,
private val bookmarkTapped: (Session) -> Unit, private val bookmarkTapped: (String, String) -> Unit,
private val scope: CoroutineScope, private val scope: CoroutineScope,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val topSitesStorage: DefaultTopSitesStorage private val topSitesStorage: DefaultTopSitesStorage,
private val browserStore: BrowserStore
) : BrowserToolbarMenuController { ) : BrowserToolbarMenuController {
private val currentSession private val currentSession
@ -184,7 +187,7 @@ class DefaultBrowserToolbarMenuController(
val directions = NavGraphDirections.actionGlobalShareFragment( val directions = NavGraphDirections.actionGlobalShareFragment(
data = arrayOf( data = arrayOf(
ShareData( ShareData(
url = currentSession?.url, url = getProperUrl(currentSession),
title = currentSession?.title title = currentSession?.title
) )
), ),
@ -270,7 +273,7 @@ class DefaultBrowserToolbarMenuController(
} }
ToolbarMenu.Item.Bookmark -> { ToolbarMenu.Item.Bookmark -> {
sessionManager.selectedSession?.let { sessionManager.selectedSession?.let {
bookmarkTapped(it) getProperUrl(it)?.let { url -> bookmarkTapped(url, it.title) }
} }
} }
ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically {
@ -295,6 +298,17 @@ class DefaultBrowserToolbarMenuController(
} }
} }
private fun getProperUrl(currentSession: Session?): String? {
return currentSession?.id?.let {
val currentTab = browserStore.state.findTab(it)
if (currentTab?.readerState?.active == true) {
currentTab.readerState.activeUrl
} else {
currentSession.url
}
}
}
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
val eventItem = when (item) { val eventItem = when (item) {

@ -21,6 +21,7 @@ import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_browser_top_toolbar.* import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.* 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.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.browser.state.state.ExternalAppType
@ -49,6 +50,7 @@ interface BrowserToolbarViewInteractor {
fun onReaderModePressed(enabled: Boolean) fun onReaderModePressed(enabled: Boolean)
} }
@ExperimentalCoroutinesApi
@SuppressWarnings("LargeClass") @SuppressWarnings("LargeClass")
class BrowserToolbarView( class BrowserToolbarView(
private val container: ViewGroup, private val container: ViewGroup,

@ -125,7 +125,7 @@ class DefaultToolbarMenu(
} }
val share = BrowserMenuItemToolbar.Button( val share = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.mozac_ic_share, imageResource = R.drawable.ic_share_filled,
contentDescription = context.getString(R.string.browser_menu_share), contentDescription = context.getString(R.string.browser_menu_share),
iconTintColorResource = primaryTextColor(), iconTintColorResource = primaryTextColor(),
listener = { listener = {
@ -134,6 +134,7 @@ class DefaultToolbarMenu(
) )
registerForIsBookmarkedUpdates() registerForIsBookmarkedUpdates()
val bookmark = BrowserMenuItemToolbar.TwoStateButton( val bookmark = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = R.drawable.ic_bookmark_filled, primaryImageResource = R.drawable.ic_bookmark_filled,
primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark), primaryContentDescription = context.getString(R.string.browser_menu_edit_bookmark),

@ -5,36 +5,46 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import android.view.View import android.view.View
import mozilla.components.browser.session.SelectionAwareSessionObserver import kotlinx.coroutines.CoroutineScope
import mozilla.components.browser.session.Session import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.SessionManager import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
@ExperimentalCoroutinesApi
class MenuPresenter( class MenuPresenter(
private val menuToolbar: BrowserToolbar, private val menuToolbar: BrowserToolbar,
sessionManager: SessionManager, private val store: BrowserStore,
private val sessionId: String? = null private val sessionId: String? = null
) : SelectionAwareSessionObserver(sessionManager), View.OnAttachStateChangeListener { ) : View.OnAttachStateChangeListener {
private var scope: CoroutineScope? = null
fun start() { fun start() {
observeIdOrSelected(sessionId)
menuToolbar.addOnAttachStateChangeListener(this) menuToolbar.addOnAttachStateChangeListener(this)
} scope = store.flowScoped { flow ->
flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) }
/** Redraw the refresh/stop button */ .ifAnyChanged { tab ->
override fun onLoadingStateChanged(session: Session, loading: Boolean) { arrayOf(
tab.content.loading,
tab.content.canGoBack,
tab.content.canGoForward,
tab.content.webAppManifest
)
}
.collect {
invalidateActions() invalidateActions()
} }
}
/** Redraw the back and forward buttons */
override fun onNavigationStateChanged(session: Session, canGoBack: Boolean, canGoForward: Boolean) {
invalidateActions()
} }
/** Redraw the install web app button */ fun stop() {
override fun onWebAppManifestChanged(session: Session, manifest: WebAppManifest?) { scope?.cancel()
invalidateActions()
} }
fun invalidateActions() { fun invalidateActions() {

@ -9,6 +9,7 @@ import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.EngineView.InputResult.INPUT_RESULT_UNHANDLED import mozilla.components.concept.engine.EngineView.InputResult.INPUT_RESULT_UNHANDLED
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -16,6 +17,7 @@ import org.mozilla.fenix.ext.settings
/** /**
* ScrollingViewBehavior that will setScrollFlags on BrowserToolbar based on EngineView touch handling * ScrollingViewBehavior that will setScrollFlags on BrowserToolbar based on EngineView touch handling
*/ */
@ExperimentalCoroutinesApi
class SwipeRefreshScrollingViewBehavior( class SwipeRefreshScrollingViewBehavior(
context: Context, context: Context,
attrs: AttributeSet?, attrs: AttributeSet?,

@ -11,6 +11,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.toolbar.Toolbar import mozilla.components.concept.toolbar.Toolbar
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.res.resolveAttribute import mozilla.components.support.ktx.android.content.res.resolveAttribute
@ -24,7 +26,6 @@ import java.lang.ref.WeakReference
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class TabCounterToolbarButton( class TabCounterToolbarButton(
private val lifecycleOwner: LifecycleOwner, private val lifecycleOwner: LifecycleOwner,
private val isPrivate: Boolean,
private val onItemTapped: (TabCounterMenu.Item) -> Unit = {}, private val onItemTapped: (TabCounterMenu.Item) -> Unit = {},
private val showTabs: () -> Unit private val showTabs: () -> Unit
) : Toolbar.Action { ) : Toolbar.Action {
@ -37,7 +38,7 @@ class TabCounterToolbarButton(
val settings = parent.context.components.settings val settings = parent.context.components.settings
store.flowScoped(lifecycleOwner) { flow -> store.flowScoped(lifecycleOwner) { flow ->
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate).size } flow.map { state -> state.getNormalOrPrivateTabs(isPrivate(store)).size }
.ifChanged() .ifChanged()
.collect { tabs -> updateCount(tabs) } .collect { tabs -> updateCount(tabs) }
} }
@ -58,7 +59,7 @@ class TabCounterToolbarButton(
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View?) { override fun onViewAttachedToWindow(v: View?) {
setCount(store.state.getNormalOrPrivateTabs(isPrivate).size) setCount(store.state.getNormalOrPrivateTabs(isPrivate(store)).size)
} }
override fun onViewDetachedFromWindow(v: View?) { /* no-op */ } override fun onViewDetachedFromWindow(v: View?) { /* no-op */ }
@ -77,4 +78,8 @@ class TabCounterToolbarButton(
private fun updateCount(count: Int) { private fun updateCount(count: Int) {
reference.get()?.setCountWithAnimation(count) reference.get()?.setCountWithAnimation(count)
} }
private fun isPrivate(store: BrowserStore): Boolean {
return store.state.selectedTab?.content?.private ?: false
}
} }

@ -8,6 +8,7 @@ import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
@ -24,6 +25,7 @@ import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
@ExperimentalCoroutinesApi
abstract class ToolbarIntegration( abstract class ToolbarIntegration(
context: Context, context: Context,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
@ -45,7 +47,7 @@ abstract class ToolbarIntegration(
) )
private val menuPresenter = private val menuPresenter =
MenuPresenter(toolbar, context.components.core.sessionManager, sessionId) MenuPresenter(toolbar, context.components.core.store, sessionId)
init { init {
toolbar.display.menuBuilder = toolbarMenu.menuBuilder toolbar.display.menuBuilder = toolbarMenu.menuBuilder
@ -67,6 +69,7 @@ abstract class ToolbarIntegration(
} }
} }
@ExperimentalCoroutinesApi
class DefaultToolbarIntegration( class DefaultToolbarIntegration(
context: Context, context: Context,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
@ -138,7 +141,6 @@ class DefaultToolbarIntegration(
val tabsAction = TabCounterToolbarButton( val tabsAction = TabCounterToolbarButton(
lifecycleOwner, lifecycleOwner,
isPrivate,
onItemTapped = { onItemTapped = {
interactor.onTabCounterMenuItemTapped(it) interactor.onTabCounterMenuItemTapped(it)
}, },

@ -5,11 +5,13 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.content.Context import android.content.Context
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.feature.toolbar.ToolbarFeature import mozilla.components.feature.toolbar.ToolbarFeature
import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarMenu
@ExperimentalCoroutinesApi
class CustomTabToolbarIntegration( class CustomTabToolbarIntegration(
context: Context, context: Context,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,

@ -4,6 +4,7 @@
package org.mozilla.fenix.customtabs package org.mozilla.fenix.customtabs
import android.content.Intent
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
@ -50,6 +51,10 @@ open class ExternalAppBrowserActivity : HomeActivity() {
// No-op for external app // No-op for external app
} }
override fun handleNewIntent(intent: Intent) {
// No-op for external app
}
override fun getNavDirections( override fun getNavDirections(
from: BrowserDirection, from: BrowserDirection,
customTabSessionId: String? customTabSessionId: String?

@ -9,7 +9,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.CustomTabConfig import mozilla.components.browser.state.state.CustomTabConfig
@ -30,6 +29,7 @@ import mozilla.components.support.utils.toSafeIntent
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -61,7 +61,7 @@ class FennecWebAppIntentProcessor(
val url = safeIntent.dataString val url = safeIntent.dataString
return if (!url.isNullOrEmpty() && matches(intent)) { return if (!url.isNullOrEmpty() && matches(intent)) {
val webAppManifest = runBlocking { loadManifest(safeIntent, url) } val webAppManifest = runBlockingIncrement { loadManifest(safeIntent, url) }
val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN) val session = Session(url, private = false, source = SessionState.Source.HOME_SCREEN)
session.webAppManifest = webAppManifest session.webAppManifest = webAppManifest

@ -0,0 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ext
import java.util.concurrent.atomic.AtomicInteger
/**
* Increases an AtomicInteger safely.
*/
fun AtomicInteger.getAndIncrementNoOverflow() {
var prev: Int
var next: Int
do {
prev = this.get()
next = if (prev == Integer.MAX_VALUE) prev else prev + 1
} while (!this.compareAndSet(prev, next))
}

@ -11,13 +11,13 @@ import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.core.net.toUri import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request import mozilla.components.concept.fetch.Request
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.publicsuffixlist.ext.urlToTrimmedHost import mozilla.components.lib.publicsuffixlist.ext.urlToTrimmedHost
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.IOException import java.io.IOException
import java.net.IDN import java.net.IDN
import java.util.Locale import java.util.Locale
@ -92,7 +92,8 @@ private fun Uri.isIpv6(): Boolean {
/** /**
* Trim a host's prefix and suffix * Trim a host's prefix and suffix
*/ */
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking { fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String =
runBlockingIncrement {
urlToTrimmedHost(publicSuffixList).await() urlToTrimmedHost(publicSuffixList).await()
} }

@ -2,12 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
@file:Suppress("TooManyFunctions")
package org.mozilla.fenix.ext package org.mozilla.fenix.ext
import android.graphics.Rect import android.graphics.Rect
import android.os.Build import android.os.Build
import android.view.TouchDelegate import android.view.TouchDelegate
import android.view.View import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.Dimension import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP import androidx.annotation.Dimension.DP
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -33,6 +36,72 @@ fun View.removeTouchDelegate() {
} }
} }
/**
* Sets the new a11y parent.
*/
fun View.setNewAccessibilityParent(newParent: View) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfo?
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.setParent(newParent)
}
}
}
/**
* Updates the a11y collection item info for an item in a list.
*/
fun View.updateAccessibilityCollectionItemInfo(
rowIndex: Int,
columnIndex: Int,
isSelected: Boolean,
rowSpan: Int = 1,
columnSpan: Int = 1
) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfo?
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.collectionItemInfo =
AccessibilityNodeInfo.CollectionItemInfo.obtain(
rowIndex,
rowSpan,
columnIndex,
columnSpan,
false,
isSelected
)
}
}
}
/**
* Updates the a11y collection info for a list.
*/
fun View.updateAccessibilityCollectionInfo(
rowCount: Int,
columnCount: Int
) {
this.accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfo?
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain(
rowCount,
columnCount,
false
)
}
}
}
/** /**
* Fills a [Rect] with data about a view's location in the screen. * Fills a [Rect] with data about a view's location in the screen.
* *
@ -41,10 +110,12 @@ fun View.removeTouchDelegate() {
*/ */
fun View.getRectWithScreenLocation(): Rect { fun View.getRectWithScreenLocation(): Rect {
val locationOnScreen = IntArray(2).apply { getLocationOnScreen(this) } val locationOnScreen = IntArray(2).apply { getLocationOnScreen(this) }
return Rect(locationOnScreen[0], return Rect(
locationOnScreen[0],
locationOnScreen[1], locationOnScreen[1],
locationOnScreen[0] + width, locationOnScreen[0] + width,
locationOnScreen[1] + height) locationOnScreen[1] + height
)
} }
/** /**

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

Loading…
Cancel
Save