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.
/.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
*proguard* @mozilla-mobile/Performance
# Possible startup regressions
*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
# about to be enabled.

@ -1,67 +1,76 @@
## Overview ##
| Monday | Tuesday | Wednesday | Thursday | Friday |
|-----------------|---------------------------|------------------------------|----------------|-------------|
| (Sprint 1 Start)| | | | |
| | Hard code freeze for Beta | | | Code Freeze/Planning
| Sprint 2 Start / Release to Beta / Release Production in Play Store 1% | QA Beta / Promote Release 25% | Promote Release 100% | | |
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 |
|-----------------|---------------------------|--------------------------------|-------------------|-----------------|
| (week 1) | | (Y.2 sprint ended) |Sprint **X.1** starts | |
| (week 2) | | | | |
| (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
- Jira account
- JIRA access
- Bugzilla account
- Google Play access (for reviewing crashes)
- Sentry access
## 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.
We will refer to the beta release going out as the *current* sprint release.
There are two releases this covers: the current sprint that is going out to Beta, and the previous Beta that is going to Production.
## Start of sprint [Monday, 1st week of sprint]
- [ ] Create milestone for *upcoming* sprint release. (e.g. if you are doing releng for v2.2, create v2.3 milestone)
- [ ] 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.
## Start of Sprint X.1 [Thursday, 1st week of sprint]
- [ ] [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.
## Release Day [Monday, 3rd week] Beta & Production
- [ ] 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.
## Sprint X.1 End [Wednesday, 2nd week] Cutting a 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.
- [ ] 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)
- 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):
- [ ] 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.
- [ ] 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 v85"
- [ ] Update the title to include this AC version "Releng for v[release] with AC [version]"
- 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.
- [ ] 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.
- 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.
- [ ] Create a new PI (product integrity) request in Jira. You can clone [this issue](https://jira.mozilla.com/browse/PI-219).
### SUMO Verification [After Beta release]
- [ ] 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.
- [ ] Create a GitHub pre-release [Release](https://github.com/mozilla-mobile/fenix/releases) with:
- [ ] 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)
- [ ] 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.
- 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.
- [ ] 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 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]
- [ ] Check Sentry for new crashes. File issues and triage.
- [ ] 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)
### Uplifting L10N strings to Beta [Wednesday, 2 weeks after sprint end]
- [ ] 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).
- [ ] 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
- [ ] Automate assigning milestones to closed issues (based on date, etc) #6199
- [ ] 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?
- [ ] Check Sentry for new crashes. File issues and triage.
- [ ] Each day, bump the release rollout if nothing concerning (5%, 20%, 100%)

@ -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:
taskgraph:
branch: taskgraph
revision: 12992b0f984884ec2b0a7bdedc3b3ba467363eb4
revision: 2b2622598df02bde211d8cedb334b7b22fb883a4
trustDomain: mobile
in:
$let:
@ -104,7 +104,7 @@ tasks:
tasks_for in ["action", "cron"]
|| (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"])
|| (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp")
|| (tasks_for == "github-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:
$let:
level:
@ -243,7 +243,7 @@ tasks:
# Note: This task is built server side without the context or tooling that
# exist in tree so we must hard code the hash
image:
mozillareleases/taskgraph:decision-mobile-6607973bc60e32323a541861cc5856cd6a0f51ea9fd664ef7d43bca8df53db47@sha256:8c471aacc469ea8e7bb4846c16efe086f7350a5cc1df570cc6c86b22895a2456
mozillareleases/taskgraph:decision-mobile-682fbaa1ef17e70ddfe3457da3eaf8e776c4a20fe5bfbdbeba0641fd5bceae2a@sha256:bbb2613aaab79d17e590fbd78c072d0643be40fd1237195703f84280ecc3b302
maxRunTime: 1800
@ -261,12 +261,13 @@ tasks:
$if: 'tasks_for == "action"'
then: >
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 &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph action-callback
else: >
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 &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph decision

@ -37,6 +37,13 @@ android {
manifestPlaceholders = [
"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 = {
@ -206,6 +213,17 @@ android {
testOptions {
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
- erichards@mozilla.com
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:
type: event
description:
@ -569,13 +554,15 @@ context_menu:
```
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:
- https://github.com/mozilla-mobile/fenix/issues/957
- https://github.com/mozilla-mobile/fenix/issues/16076
data_reviews:
- 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/issues/16076#issuecomment-726216734
data_sensitivity:
- interaction
notification_emails:
@ -698,6 +685,23 @@ metrics:
notification_emails:
- fenix-core@mozilla.com
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:
type: counter
lifetime: application
@ -1659,67 +1663,6 @@ activation:
no_lint:
- 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:
visited_error:
type: event
@ -1858,22 +1801,6 @@ sync_auth:
notification_emails:
- fenix-core@mozilla.com
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:
type: event
description: |
@ -1936,20 +1863,6 @@ sync_account:
notification_emails:
- fenix-core@mozilla.com
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:
type: event
description: |
@ -2306,8 +2219,7 @@ tabs_tray:
save_to_collection:
type: event
description: |
A user tapped the save to collection button in the
three dot menu within the tabs tray
A user tapped the save to collection button in the tabs tray
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11273
data_reviews:
@ -2591,81 +2503,7 @@ search_widget:
- fenix-core@mozilla.com
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:
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:
type: event
description: |
@ -2695,35 +2533,6 @@ private_browsing_mode:
notification_emails:
- fenix-core@mozilla.com
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:
display:

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

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

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

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

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

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

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

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

@ -13,7 +13,6 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.ui.robots.PRIVATE_SESSION_MESSAGE
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
fun privateModeScreenItemsTest() {
homeScreen { }.dismissOnboarding()
@ -136,7 +73,7 @@ class HomeScreenTest {
homeScreen {
// To deal with the race condition where multiple "add tab" buttons are present,
// 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()
verifyNavigationToolbar()
verifyHomePrivateBrowsingButton()

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

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

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

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

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

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

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

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

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

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

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

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

@ -4,12 +4,12 @@
package org.mozilla.fenix.ui
import androidx.core.net.toUri
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -34,7 +34,7 @@ class SmokeTest {
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher())
dispatcher = AndroidAssetDispatcher()
start()
}
}
@ -44,6 +44,56 @@ class SmokeTest {
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
fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -58,15 +108,14 @@ class SmokeTest {
}.openTabDrawer {
verifyExistingTabList()
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyHomeScreen()
}
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/13217")
@Test
fun verifyPageMainMenuItemsListInPortraitNormalModeTest() {
fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// Add this to check openInApp and youtube is a default app available in every Android emulator/device
val youtubeUrl = "www.youtube.com"
@ -75,24 +124,16 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
verifyThreeDotMainMenuItems()
verifySaveCollection()
}.clickAddOnsReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
}.openThreeDotMenu {
}.openHistory {
verifyTestPageUrl(defaultWebPage.url)
verifyHistoryMenuView()
}.goBackToBrowser {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
verifyEmptyBookmarksList()
}.goBackToBrowser {
}.openThreeDotMenu {
}.openSyncedTabs {
verifyNavigationToolBarHeader()
verifySyncedTabsStatus()
verifySyncedTabsMenuHeader()
}.goBack {
}.openThreeDotMenu {
}.openSettings {
@ -107,7 +148,7 @@ class SmokeTest {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesTabs(defaultWebPage.title)
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
@ -120,7 +161,7 @@ class SmokeTest {
}.openThreeDotMenu {
}.openSaveToCollection {
verifyCollectionNameTextField()
}.goBackToBrowser {
}.exitSaveCollection {
}.openThreeDotMenu {
}.bookmarkPage {
verifySnackBarText("Bookmark saved!")
@ -131,96 +172,15 @@ class SmokeTest {
}.openThreeDotMenu {
}.refreshPage {
verifyUrl(defaultWebPage.url.toString())
}.openTabDrawer {
closeTabViaXButton(defaultWebPage.title)
}.openNewTab {
}.submitQuery(youtubeUrl) {
verifyBlueDot()
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(youtubeUrl.toUri()) {
}.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 {
}.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("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12899")
@Test
fun verifyETPToolbarShieldIconIsNotDisplayedIfETPIsOFFGloballyTest() {
fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
@ -234,19 +194,11 @@ class SmokeTest {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
verifyEnhancedTrackingProtectionPanelNotVisible()
}.openThreeDotMenu {
}.clickAddOnsReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
verifyTabCounter("2")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
clickEnhancedTrackingProtectionDefaults()
}.goBackToHomeScreen {
}.openTabDrawer {
}.openTab(defaultWebPage.title) {
}.goBack {
}.goBackToBrowser {
clickEnhancedTrackingProtectionPanel()
verifyEnhancedTrackingProtectionSwitch()
// Turning off TP Switch results in adding the WebPage to exception list

@ -42,7 +42,7 @@ class StrictEnhancedTrackingProtectionTest {
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher())
dispatcher = AndroidAssetDispatcher()
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.uiautomator.UiDevice
import com.google.android.material.bottomsheet.BottomSheetBehavior
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
@ -49,7 +50,7 @@ class TabbedBrowsingTest {
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher())
dispatcher = AndroidAssetDispatcher()
start()
}
}
@ -84,7 +85,7 @@ class TabbedBrowsingTest {
}.openTabsListThreeDotMenu {
verifyCloseAllTabsButton()
verifyShareTabButton()
verifySaveCollection()
verifySelectTabs()
}
}
@ -126,7 +127,7 @@ class TabbedBrowsingTest {
}.openTabsListThreeDotMenu {
verifyCloseAllTabsButton()
verifyShareTabButton()
verifySaveCollection()
verifySelectTabs()
}.closeAllTabs {
verifyNoTabsOpened()
}
@ -187,7 +188,7 @@ class TabbedBrowsingTest {
}.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1")
}.openNewTab {
}.dismiss { }
}.dismissSearchBar { }
}
@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
fun verifyEmptyTabTray() {
homeScreen { }.dismissOnboarding()
@ -286,7 +312,7 @@ class TabbedBrowsingTest {
verifyExistingOpenTabs(defaultWebPage.title)
verifyCloseTabsButton(defaultWebPage.title)
}.openNewTab {
}.dismiss { }
}.dismissSearchBar { }
}
@Test

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

@ -36,7 +36,7 @@ class TopSitesTest {
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
setDispatcher(AndroidAssetDispatcher())
dispatcher = AndroidAssetDispatcher()
start()
}
}
@ -59,7 +59,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}
@ -78,14 +78,14 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openTopSiteTabWithTitle(title = defaultWebPageTitle) {
verifyUrl(defaultWebPage.url.toString().replace("http://", ""))
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {
@ -109,7 +109,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(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
fun verifyRemoveTopSite() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -132,7 +157,7 @@ class TopSitesTest {
verifySnackBarText("Added to top sites!")
}.openTabDrawer {
}.openNewTab {
}.dismiss {
}.dismissSearchBar {
verifyExistingTopSitesList()
verifyExistingTopSitesTabs(defaultWebPageTitle)
}.openContextMenuOnTopSitesWithTitle(defaultWebPageTitle) {

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

@ -33,6 +33,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matchers.not
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
@ -155,8 +156,6 @@ class BrowserRobot {
fun verifyMenuButton() = assertMenuButton()
fun verifyBlueDot() = assertBlueDot()
fun verifyNavURLBarItems() {
verifyEnhancedTrackingOptions()
pressBack()
@ -187,10 +186,10 @@ class BrowserRobot {
.perform(ViewActions.pressBack())
}
fun clickEnhancedTrackingProtectionPanel() = enhancedTrackingProtectionPanel().click()
fun clickEnhancedTrackingProtectionPanel() = enhancedTrackingProtectionIndicator().click()
fun verifyEnhancedTrackingProtectionPanelNotVisible() =
assertEnhancedTrackingProtectionPanelNotVisible()
assertEnhancedTrackingProtectionIndicatorNotVisible()
fun clickContextOpenLinkInNewTab() {
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()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
fun enhancedTrackingProtectionPanel() =
fun enhancedTrackingProtectionIndicator() =
onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator))
private fun assertEnhancedTrackingProtectionPanelNotVisible() {
enhancedTrackingProtectionPanel()
.check(matches(withEffectiveVisibility(Visibility.GONE)))
private fun assertEnhancedTrackingProtectionIndicatorNotVisible() {
enhancedTrackingProtectionIndicator().check(matches(not(isDisplayed())))
}
private fun assertEnhancedTrackingProtectionSwitch() {
@ -453,10 +451,3 @@ private fun mediaPlayerPlayButton() =
.className("android.widget.Button")
.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()
}
fun verifyHistoryListExists() = assertHistoryListExists()
fun verifyVisitedTimeTitle() {
mDevice.waitNotNull(
Until.findObject(
@ -85,6 +87,8 @@ class HistoryRobot {
class Transition {
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
@ -131,6 +135,9 @@ private fun assertEmptyHistoryView() =
)
.check(matches(withText("No history here")))
private fun assertHistoryListExists() =
mDevice.findObject(UiSelector().resourceId("R.id.history_list")).waitForExists(waitingTime)
private fun assertVisitedTimeTitle() =
onView(withId(R.id.header_title)).check(matches(withText("Today")))

@ -7,6 +7,7 @@
package org.mozilla.fenix.ui.robots
import android.graphics.Bitmap
import android.widget.EditText
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
@ -36,14 +37,18 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
import org.hamcrest.CoreMatchers.not
import org.hamcrest.Matchers
import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.components.Search
import org.mozilla.fenix.helpers.TestAssetHelper
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.ext.waitNotNull
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.
*/
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 verifyFocusedNavigationToolbar() = assertFocusedNavigationToolbar()
fun verifyHomeScreen() = assertHomeScreen()
@ -72,7 +83,7 @@ class HomeScreenRobot {
// First Run elements
fun verifyWelcomeHeader() = assertWelcomeHeader()
fun verifyGetTheMostHeader() = assertGetTheMostHeader()
fun verifyStartSyncHeader() = assertStartSyncHeader()
fun verifyAccountsSignInButton() = assertAccountsSignInButton()
fun verifyGetToKnowHeader() = assertGetToKnowHeader()
fun verifyChooseThemeHeader() = assertChooseThemeHeader()
@ -83,14 +94,10 @@ class HomeScreenRobot {
fun verifyDarkThemeDescription() = assertDarkThemeDescription()
fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle()
fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription()
fun verifyAutomaticPrivacyfHeader() = assertAutomaticPrivacyHeader()
fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader()
fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle()
fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText()
// What's new elements
fun verifyWhatsNewHeader() = assertWhatsNewHeather()
fun verifyWhatsNewLink() = assertWhatsNewLink()
// Browse privately
fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader()
fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText()
@ -445,6 +452,18 @@ class HomeScreenRobot {
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 {
onView(withText("Remove"))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
@ -569,10 +588,10 @@ private fun verifySearchEngineIcon(searchEngineName: String) {
// First Run elements
private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to Firefox Preview!")))
onView(allOf(withText("Welcome to ${appContext.appName}!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetTheMostHeader() =
private fun assertStartSyncHeader() =
onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -581,51 +600,69 @@ private fun assertAccountsSignInButton() =
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() =
onView(allOf(withText("Get to know Firefox Preview")))
onView(allOf(withText("Get to know ${appContext.appName}")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertChooseThemeHeader() =
onView(allOf(withText("Choose your theme")))
private fun assertChooseThemeHeader() {
scrollToElementByText("Choose your theme")
onView(withText("Choose your theme"))
.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.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeToggle() =
private fun assertLightThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_light_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertLightThemeDescription() =
private fun assertLightThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Light theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeToggle() =
private fun assertDarkThemeToggle() {
scrollToElementByText("Choose your theme")
onView(ViewMatchers.withResourceName("theme_dark_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDarkThemeDescription() =
private fun assertDarkThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Dark theme")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAutomaticThemeToggle() =
}
private fun assertAutomaticThemeToggle() {
scrollToElementByText("Choose your theme")
onView(withId(R.id.theme_automatic_radio_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticThemeDescription() =
private fun assertAutomaticThemeDescription() {
scrollToElementByText("Choose your theme")
onView(allOf(withText("Automatic")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyHeader() =
private fun assertAutomaticPrivacyHeader() {
scrollToElementByText("Automatic privacy")
onView(allOf(withText("Automatic privacy")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTrackingProtectionToggle() = onView(
allOf(ViewMatchers.withResourceName("tracking_protection_toggle"))
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertTrackingProtectionToggle() {
scrollToElementByText("Automatic privacy")
onView(withId(R.id.tracking_protection_toggle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyText() {
scrollToElementByText("Automatic privacy")
onView(
allOf(
withText(
@ -636,60 +673,65 @@ private fun assertAutomaticPrivacyText() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertBrowsePrivatelyHeader() =
private fun assertBrowsePrivatelyHeader() {
scrollToElementByText("Browse privately")
onView(allOf(withText("Browse privately")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertBrowsePrivatelyText() =
private fun assertBrowsePrivatelyText() {
scrollToElementByText("Browse privately")
onView(allOf(withText(containsString("Update your private browsing settings."))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertYourPrivacyHeader() =
}
private fun assertYourPrivacyHeader() {
scrollToElementByText("Your privacy")
onView(allOf(withText("Your privacy")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyText() =
private fun assertYourPrivacyText() {
scrollToElementByText("Your privacy")
onView(
allOf(
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)))
}
private fun assertPrivacyNoticeButton() =
private fun assertPrivacyNoticeButton() {
scrollToElementByText("Your privacy")
onView(allOf(withText("Read our privacy notice")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// What's new elements
private fun assertWhatsNewHeather() = onView(allOf(withText("See whats new")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertWhatsNewLink() = onView(allOf(withText("Get answers here")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartBrowsingButton() =
private fun assertStartBrowsingButton() {
scrollToElementByText("Start browsing")
onView(allOf(withText("Start browsing")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// Take a position
private fun assertTakePositionheader() = onView(allOf(withText("Take a position")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertTakePositionheader() {
scrollToElementByText("Take a position")
onView(allOf(withText("Take a position")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionTopRadioButton() =
private fun assertTakePositionTopRadioButton() {
scrollToElementByText("Take a position")
onView(ViewMatchers.withResourceName("toolbar_top_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionBottomRadioButton() =
private fun assertTakePositionBottomRadioButton() {
scrollToElementByText("Take a position")
onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button"))
.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() =
onView(withId(R.id.private_session_description))

@ -126,7 +126,7 @@ class SearchRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
fun dismiss(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
fun dismissSearchBar(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
mDevice.waitForIdle()
mDevice.pressBack()
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.hasDescendant
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.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
@ -425,15 +426,17 @@ private fun assertAboutHeading(): 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"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
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"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isDisplayed()))
}
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.matcher.ViewMatchers.withContentDescription
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.Visibility
import androidx.test.platform.app.InstrumentationRegistry
@ -22,9 +21,7 @@ import org.mozilla.fenix.helpers.click
*/
class SyncedTabsRobot {
fun verifyNavigationToolBarHeader() = assertNavigationToolBarHeader()
fun verifySyncedTabsStatus() = assertSyncedTabsStatus()
fun verifySyncedTabsMenuHeader() = assertSyncedTabsMenuHeader()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
@ -41,12 +38,7 @@ class SyncedTabsRobot {
private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up")))
private fun assertNavigationToolBarHeader() {
private fun assertSyncedTabsMenuHeader() {
onView(withText(R.string.synced_tabs))
.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
import android.content.Context
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.Espresso
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
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.contrib.RecyclerViewActions
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.Until
import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matcher
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click
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.
@ -52,6 +61,10 @@ class TabDrawerRobot {
fun verifyNewTabButton() = assertNewTabButton()
fun verifyTabTrayOverflowMenu(visibility: Boolean) = assertTabTrayOverflowButton(visibility)
fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist()
fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio()
fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState)
fun closeTab() {
closeTabButton().click()
}
@ -126,9 +139,7 @@ class TabDrawerRobot {
fun closeTabDrawer(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitForIdle(waitingTime)
// Dismisses the tab tray bottom sheet with 2 handle clicks
onView(withId(R.id.handle)).perform(
click(),
click()
)
BrowserRobot().interact()
@ -169,6 +180,52 @@ class TabDrawerRobot {
BrowserRobot().interact()
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))
.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) =
onView(
allOf(

@ -15,6 +15,7 @@ import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
@ -68,6 +69,7 @@ class ThreeDotMenuMainRobot {
fun verifyShareTabButton() = assertShareTabButton()
fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton()
fun clickBrowserViewSaveCollectionButton() {
browserViewSaveCollectionButton().click()
@ -114,9 +116,11 @@ class ThreeDotMenuMainRobot {
fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite()
fun verifyOpenInAppButton() = assertOpenInAppButton()
fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyThreeDotMainMenuItems() {
verifyAddOnsButton()
verifyDownloadsButton()
verifyHistoryButton()
verifyBookmarksButton()
verifySyncedTabsButton()
@ -125,6 +129,7 @@ class ThreeDotMenuMainRobot {
verifyAddFirefoxHome()
verifyAddToMobileHome()
verifyDesktopSite()
verifySaveCollection()
verifyAddBookmarkButton()
verifyShareButton()
verifyForwardButton()
@ -135,14 +140,6 @@ class ThreeDotMenuMainRobot {
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 {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings)))
@ -224,13 +221,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.pressBack()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun close(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
// Close three dot
mDevice.pressBack()
@ -359,6 +349,13 @@ class ThreeDotMenuMainRobot {
SettingsSubMenuAddonsManagerRobot().interact()
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()))
private fun addOnsButton() = onView(allOf(withText("Add-ons")))
private fun assertAddOnsButton() = addOnsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addOnsReportSiteIssueButton() = onView(allOf(withText("Report Site Issue…")))
private fun assertAddOnsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun historyButton() = onView(allOf(withText(R.string.library_history)))
private fun assertHistoryButton() = historyButton()
@ -397,8 +395,10 @@ private fun assertForwardButton() = forwardButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark"))
private fun assertAddBookmarkButton() = addBookmarkButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertAddBookmarkButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark"))
private fun assertEditBookmarkButton() = editBookmarkButton()
@ -431,6 +431,10 @@ private fun saveCollectionButton() = onView(allOf(withText("Save to collection")
private fun assertSaveCollectionButton() = saveCollectionButton()
.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 assertaddNewCollectionButton() = addNewCollectionButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -454,10 +458,10 @@ private fun SendToDeviceTitle() =
private fun assertSendToDeviceTitle() = SendToDeviceTitle()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun ShareALinkTitle() =
private fun shareALinkTitle() =
onView(allOf(withText("ALL ACTIONS"), withResourceName("apps_link_header")))
private fun assertShareALinkTitle() = ShareALinkTitle()
private fun assertShareALinkTitle() = shareALinkTitle()
private fun whatsNewButton() = onView(
allOf(
@ -511,12 +515,8 @@ private fun assertAddToMobileHome() {
private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site)))
private fun assertDesktopSite() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
hasDescendant(withText(R.string.browser_menu_desktop_site))
)
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
desktopSiteButton().check(matches(isDisplayed()))
}
private fun openInAppButton() =
@ -530,9 +530,15 @@ private fun assertOpenInAppButton() {
).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() {
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 org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
object GeckoProvider {
private var runtime: GeckoRuntime? = null
const val CN_UPDATE_URL =
"https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
const val CN_GET_HASH_URL =
"https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
@Synchronized
fun getOrCreateRuntime(
@ -53,6 +59,28 @@ object GeckoProvider {
runtimeSettings.fontSizeFactor = fontSize
}
// Add safebrowsing providers for China
if (Config.channel.isMozillaOnline) {
val mozcn = SafeBrowsingProvider
.withName("mozcn")
.version("2.2")
.lists("m6eb-phish-shavar", "m6ib-phish-shavar")
.updateUrl(CN_UPDATE_URL)
.getHashUrl(CN_GET_HASH_URL)
.build()
runtimeSettings.contentBlocking.setSafeBrowsingProviders(mozcn,
// Keep the existing configuration
ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER,
ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
runtimeSettings.contentBlocking.setSafeBrowsingPhishingTable(
"m6eb-phish-shavar",
"m6ib-phish-shavar",
// Existing configuration
"goog-phish-proto")
}
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)

@ -14,9 +14,15 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.components
import org.mozilla.geckoview.GeckoRuntime
import org.mozilla.geckoview.GeckoRuntimeSettings
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.ContentBlocking.SafeBrowsingProvider
object GeckoProvider {
private var runtime: GeckoRuntime? = null
const val CN_UPDATE_URL =
"https://sb.firefox.com.cn/downloads?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
const val CN_GET_HASH_URL =
"https://sb.firefox.com.cn/gethash?client=SAFEBROWSING_ID&appver=%MAJOR_VERSION%&pver=2.2"
@Synchronized
fun getOrCreateRuntime(
@ -53,6 +59,28 @@ object GeckoProvider {
runtimeSettings.fontSizeFactor = fontSize
}
// Add safebrowsing providers for China
if (Config.channel.isMozillaOnline) {
val mozcn = SafeBrowsingProvider
.withName("mozcn")
.version("2.2")
.lists("m6eb-phish-shavar", "m6ib-phish-shavar")
.updateUrl(CN_UPDATE_URL)
.getHashUrl(CN_GET_HASH_URL)
.build()
runtimeSettings.contentBlocking.setSafeBrowsingProviders(mozcn,
// Keep the existing configuration
ContentBlocking.GOOGLE_SAFE_BROWSING_PROVIDER,
ContentBlocking.GOOGLE_LEGACY_SAFE_BROWSING_PROVIDER)
runtimeSettings.contentBlocking.setSafeBrowsingPhishingTable(
"m6eb-phish-shavar",
"m6ib-phish-shavar",
// Existing configuration
"goog-phish-proto")
}
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)

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

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

@ -34,7 +34,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
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.ktx.android.content.appName
import mozilla.components.support.ktx.android.content.res.resolveAttribute
@ -158,7 +158,7 @@ class PagedAddonInstallationDialogFragment : AppCompatDialogFragment() {
rootView.findViewById<TextView>(R.id.title).text =
requireContext().getString(
R.string.mozac_feature_addons_installed_dialog_title,
addon.translatedName,
addon.translateName(requireContext()),
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.SectionViewHolder
import mozilla.components.feature.addons.ui.CustomViewHolder.UnsupportedSectionViewHolder
import mozilla.components.feature.addons.ui.translatedName
import mozilla.components.feature.addons.ui.translatedSummary
import mozilla.components.feature.addons.ui.translateName
import mozilla.components.feature.addons.ui.translateSummary
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.res.resolveAttribute
import java.io.IOException
@ -96,7 +96,8 @@ class PagedAddonsManagerAdapter(
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(R.layout.mozac_feature_addons_section_item, parent, false)
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 {
@ -210,13 +211,13 @@ class PagedAddonsManagerAdapter(
holder.titleView.text =
if (addon.translatableName.isNotEmpty()) {
addon.translatedName
addon.translateName(context)
} else {
addon.id
}
if (addon.translatableSummary.isNotEmpty()) {
holder.summaryView.text = addon.translatedSummary
holder.summaryView.text = addon.translateSummary(context)
} else {
holder.summaryView.visibility = View.GONE
}

@ -66,6 +66,13 @@ enum class ReleaseChannel {
ForkRelease -> true
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 {

@ -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
/**
* 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.
*/
@ -45,9 +35,4 @@ object FeatureFlags {
* Enables ETP cookie purging
*/
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.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session
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.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
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
// before that process completes, we wait here, if necessary.
if (!megazordSetup.isCompleted) {
runBlocking { megazordSetup.await(); }
runBlockingIncrement { megazordSetup.await() }
}
}
@ -171,24 +171,21 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
}
fun queueInitExperiments() {
@Suppress("ControlFlowWithEmptyBody")
if (settings().isExperimentationEnabled) {
queue.runIfReadyOrQueue {
Experiments.initialize(
applicationContext = applicationContext,
onExperimentsUpdated = {
ExperimentsManager.initSearchWidgetExperiment(this)
},
onExperimentsUpdated = null,
configuration = mozilla.components.service.experiments.Configuration(
httpClient = components.core.client,
kintoEndpoint = KINTO_ENDPOINT_PROD
)
)
ExperimentsManager.initSearchWidgetExperiment(this)
}
} else {
// We should make a better way to opt out for when we have more experiments
// See https://github.com/mozilla-mobile/fenix/issues/6278
ExperimentsManager.optOutSearchWidgetExperiment(this)
}
}
@ -446,8 +443,17 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// https://issuetracker.google.com/issues/143570309#comment3
applicationContext.resources.configuration.uiMode = config.uiMode
// random StrictMode onDiskRead violation even when Fenix is not running in the background.
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
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()) {
super.onConfigurationChanged(config)
}
} else {
super.onConfigurationChanged(config)
}
}

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

@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.feature.addons.Addon
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 org.mozilla.fenix.BrowserDirection
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?) {
super.onViewCreated(view, savedInstanceState)
showToolbar(title = args.addon.translatedName)
context?.let {
showToolbar(title = args.addon.translateName(it))
}
AddonDetailsView(view, interactor = this).bind(args.addon)
}

@ -16,7 +16,7 @@ import androidx.core.text.getSpans
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.fragment_add_on_details.*
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 org.mozilla.fenix.R
import java.text.DateFormat
@ -100,7 +100,7 @@ class AddonDetailsView(
}
private fun bindDetails(addon: Addon) {
val detailsText = addon.translatedDescription
val detailsText = addon.translateDescription(containerView.context)
val parsedText = detailsText.replace("\n", "<br/>")
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.navArgs
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.ext.showToolbar
@ -33,7 +33,9 @@ class AddonInternalSettingsFragment : AddonPopupBaseFragment() {
override fun onResume() {
super.onResume()
showToolbar(args.addon.translatedName)
context?.let {
showToolbar(args.addon.translateName(it))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {

@ -9,7 +9,7 @@ import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
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.HomeActivity
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?) {
super.onViewCreated(view, savedInstanceState)
showToolbar(args.addon.translatedName)
context?.let {
showToolbar(args.addon.translateName(it))
}
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.AddonManagerException
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.PagedAddonsManagerAdapter
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.theme.ThemeManager
import java.util.Locale
import java.lang.ref.WeakReference
import java.util.concurrent.CancellationException
/**
* Fragment use for managing add-ons.
*/
@Suppress("LargeClass", "TooManyFunctions")
@Suppress("TooManyFunctions", "LargeClass")
class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management) {
/**
@ -214,7 +215,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun hasExistingAddonInstallationDialogFragment(): Boolean {
return parentFragmentManager.findFragmentByTag(INSTALLATION_DIALOG_FRAGMENT_TAG)
as? PagedAddonInstallationDialogFragment != null
as? PagedAddonInstallationDialogFragment != null
}
private fun showPermissionDialog(addon: Addon) {
@ -243,7 +244,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
private fun showInstallationDialog(addon: Addon) {
if (!isInstallationInProgress && !hasExistingAddonInstallationDialogFragment()) {
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(
addon = addon,
@ -263,7 +272,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
),
onConfirmButtonClicked = { _, allowInPrivateBrowsing ->
if (allowInPrivateBrowsing) {
requireContext().components.addonManager.setAddonAllowedInPrivateBrowsing(
weakApplicationContext.get()?.components?.addonManager?.setAddonAllowedInPrivateBrowsing(
addon,
allowInPrivateBrowsing,
onSuccess = {
@ -304,13 +313,15 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
// No need to display an error message if installation was cancelled by the user.
if (e !is CancellationException) {
val rootView = activity?.getRootView() ?: view
showSnackBar(
rootView,
getString(
R.string.mozac_feature_addons_failed_to_install,
addon.translatedName
context?.let {
showSnackBar(
rootView,
getString(
R.string.mozac_feature_addons_failed_to_install,
addon.translateName(it)
)
)
)
}
}
addonProgressOverlay?.visibility = View.GONE
isInstallationInProgress = false

@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon
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.R
import org.mozilla.fenix.ext.components
@ -85,7 +85,7 @@ class InstalledAddonDetailsFragment : Fragment() {
}
private fun bindUI(view: View) {
val title = addon.translatedName
val title = addon.translateName(view.context)
showToolbar(title)
bindEnableSwitch(view)
@ -117,13 +117,15 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.setText(R.string.mozac_feature_addons_enabled)
view.settings.isVisible = shouldSettingsBeVisible()
view.remove_add_on.isEnabled = true
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_enabled,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_enabled,
addon.translateName(it)
)
)
)
}
}
},
onError = {
@ -131,13 +133,15 @@ class InstalledAddonDetailsFragment : Fragment() {
switch.isClickable = true
view.remove_add_on.isEnabled = true
switch.setState(addon.isEnabled())
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_enable,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_enable,
addon.translateName(it)
)
)
)
}
}
}
)
@ -152,13 +156,15 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isVisible = it.isEnabled()
switch.setText(R.string.mozac_feature_addons_disabled)
view.remove_add_on.isEnabled = true
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_disabled,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_disabled,
addon.translateName(it)
)
)
)
}
}
},
onError = {
@ -167,13 +173,15 @@ class InstalledAddonDetailsFragment : Fragment() {
privateBrowsingSwitch.isClickable = true
view.remove_add_on.isEnabled = true
switch.setState(addon.isEnabled())
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_disable,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_disable,
addon.translateName(it)
)
)
)
}
}
}
)
@ -263,26 +271,30 @@ class InstalledAddonDetailsFragment : Fragment() {
onSuccess = {
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true)
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_uninstalled,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_successfully_uninstalled,
addon.translateName(it)
)
)
)
}
view.findNavController().popBackStack()
}
},
onError = { _, _ ->
runIfFragmentIsAttached {
setAllInteractiveViewsClickable(view, true)
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_uninstall,
addon.translatedName
context?.let {
showSnackBar(
view,
getString(
R.string.mozac_feature_addons_failed_to_uninstall,
addon.translateName(it)
)
)
)
}
}
}
)

@ -14,13 +14,14 @@ import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityManager
import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.net.toUri
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.google.android.material.snackbar.Snackbar
@ -30,7 +31,6 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
@ -38,12 +38,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.thumbnails.BrowserThumbnails
@ -69,6 +71,7 @@ import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.lib.state.ext.consumeFlow
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
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.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
@ -85,7 +89,6 @@ import org.mozilla.fenix.OnBackLongPressedListener
import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.FindInPageIntegration
import org.mozilla.fenix.components.StoreProvider
@ -127,19 +130,19 @@ import java.lang.ref.WeakReference
*/
@ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, SessionManager.Observer,
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore
private lateinit var browserAnimator: BrowserAnimator
private lateinit var components: Components
private var _browserInteractor: BrowserToolbarViewInteractor? = null
protected val browserInteractor: BrowserToolbarViewInteractor
get() = _browserInteractor!!
private var _browserToolbarView: BrowserToolbarView? = null
protected val browserToolbarView: BrowserToolbarView
@VisibleForTesting
internal val browserToolbarView: BrowserToolbarView
get() = _browserToolbarView!!
protected val readerViewFeature = ViewBoundFeatureWrapper<ReaderViewFeature>()
@ -166,7 +169,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
var customTabSessionId: String? = null
private var browserInitialized: Boolean = false
@VisibleForTesting
internal var browserInitialized: Boolean = false
private var initUIJob: Job? = null
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 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)
browserFragmentStore = StoreProvider.get(this) {
@ -227,16 +209,24 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
browserInitialized = initializeUI(view) != null
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())
}
observeTabSelection(requireComponents.core.store)
requireContext().accessibilityManager.addAccessibilityStateChangeListener(this)
}
private val homeViewModel: HomeScreenViewModel by activityViewModels {
ViewModelProvider.NewInstanceFactory() // this is a workaround for #4652
}
private val homeViewModel: HomeScreenViewModel by activityViewModels()
@Suppress("ComplexMethod", "LongMethod")
@CallSuper
protected open fun initializeUI(view: View): Session? {
@VisibleForTesting
internal open fun initializeUI(view: View): Session? {
val context = requireContext()
val sessionManager = context.components.core.sessionManager
val store = context.components.core.store
@ -248,14 +238,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
fragment = WeakReference(this),
engineView = WeakReference(engineView),
swipeRefresh = WeakReference(swipeRefresh),
viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope),
settings = context.components.settings,
firstContentfulHappened = ::didFirstContentfulHappen
viewLifecycleScope = WeakReference(viewLifecycleOwner.lifecycleScope)
).apply {
beginAnimateInIfNecessary()
}
return getSessionById()?.also { session ->
return getSessionById()?.also { _ ->
val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply {
action = Intent.ACTION_VIEW
putExtra(HomeActivity.OPEN_TO_BROWSER, true)
@ -316,10 +304,15 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) },
openInFenixIntent = openInFenixIntent,
bookmarkTapped = { viewLifecycleOwner.lifecycleScope.launch { bookmarkTapped(it) } },
bookmarkTapped = { url: String, title: String ->
viewLifecycleOwner.lifecycleScope.launch {
bookmarkTapped(url, title)
}
},
scope = viewLifecycleOwner.lifecycleScope,
tabCollectionStorage = requireComponents.core.tabCollectionStorage,
topSitesStorage = requireComponents.core.topSitesStorage
topSitesStorage = requireComponents.core.topSitesStorage,
browserStore = store
)
_browserInteractor = BrowserInteractor(
@ -582,7 +575,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
feature = SitePermissionsFeature(
context = context,
storage = context.components.core.permissionStorage.permissionsStorage,
sessionManager = sessionManager,
fragmentManager = parentFragmentManager,
promptsStyling = SitePermissionsFeature.PromptsStyling(
gravity = getAppropriateLayoutGravity(),
@ -598,7 +590,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
shouldShowRequestPermissionRationale(
it
)
}),
},
store = store
),
owner = this,
view = view
)
@ -631,28 +625,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
view = view
)
session.register(observer = object : Session.Observer {
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)
expandToolbarOnNavigation(store)
store.flowScoped(viewLifecycleOwner) { flow ->
flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) }
@ -660,28 +633,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
.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 =
FeatureFlags.pullToRefreshEnabled && context.settings().isPullToRefreshEnabledInBrowser
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
* 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
* download, because [DownloadsFeature] clears any queued downloads onStop.
* */
private fun resumeDownloadDialogState(
@VisibleForTesting
internal fun resumeDownloadDialogState(
sessionId: String?,
store: BrowserStore,
view: View,
@ -834,26 +801,66 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
): List<ContextMenuCandidate>
@CallSuper
override fun onSessionSelected(session: Session) {
if (!this.isRemoving) {
updateThemeForSession(session)
}
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
override fun onStart() {
super.onStart()
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)
}
}
}
}
}
@VisibleForTesting
internal fun observeTabSelection(store: BrowserStore) {
consumeFlow(store) { flow ->
flow.ifChanged {
it.selectedTabId
}
.mapNotNull {
it.selectedTab
}
.collect {
handleTabSelected(it)
}
}
}
@CallSuper
override fun onStart() {
super.onStart()
requireComponents.core.sessionManager.register(this, this, autoPause = true)
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
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
@ -868,7 +875,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
}
hideToolbar()
getSessionById()?.let { updateThemeForSession(it) }
components.core.store.state.findTabOrCustomTabOrSelectedTab(customTabSessionId)?.let {
updateThemeForSession(it)
}
}
@CallSuper
@ -992,13 +1001,13 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* Returns the layout [android.view.Gravity] for the quick settings and ETP dialog.
*/
protected fun getAppropriateLayoutGravity(): Int =
components.settings.toolbarPosition.androidGravity
requireComponents.settings.toolbarPosition.androidGravity
/**
* Updates the site permissions rules based on user settings.
*/
private fun assignSitePermissionsRules() {
val rules = components.settings.getSitePermissionsCustomSettingsRules()
val rules = requireComponents.settings.getSitePermissionsCustomSettingsRules()
sitePermissionsFeature.withFeature {
it.sitePermissionsRules = rules
@ -1033,8 +1042,9 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
/**
* Set the activity normal/private theme to match the current session.
*/
private fun updateThemeForSession(session: Session) {
val sessionMode = BrowsingMode.fromBoolean(session.private)
@VisibleForTesting
internal fun updateThemeForSession(session: SessionState) {
val sessionMode = BrowsingMode.fromBoolean(session.content.private)
(activity as HomeActivity).browsingModeManager.mode = sessionMode
}
@ -1042,7 +1052,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
* Returns the current session.
*/
protected fun getSessionById(): Session? {
val sessionManager = components.core.sessionManager
val sessionManager = requireComponents.core.sessionManager
val localCustomTabId = customTabSessionId
return if (localCustomTabId != null) {
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 existing =
bookmarksStorage.getBookmarksWithUrl(session.url).firstOrNull { it.url == session.url }
bookmarksStorage.getBookmarksWithUrl(sessionUrl).firstOrNull { it.url == sessionUrl }
if (existing != null) {
// Bookmark exists, go to edit fragment
withContext(Main) {
@ -1067,8 +1077,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
// Save bookmark, then go to edit fragment
val guid = bookmarksStorage.addItem(
BookmarkRoot.Mobile.id,
url = session.url,
title = session.title,
url = sessionUrl,
title = sessionTitle,
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) {
// Close find in page bar if opened
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
*/
@ -1205,8 +1207,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1
private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2
private const val REQUEST_CODE_APP_PERMISSIONS = 3
private const val LOADING_PROGRESS_COMPLETE = 100
}
override fun onAccessibilityStateChanged(enabled: Boolean) {

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

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

@ -10,7 +10,7 @@ import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
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.updateLayoutParams
import kotlinx.android.synthetic.main.mozac_ui_tabcounter_layout.view.*
@ -40,10 +40,9 @@ class TabPreview @JvmOverloads constructor(
gravity = Gravity.TOP
}
fakeToolbar.background = ResourcesCompat.getDrawable(
resources,
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context),
null
fakeToolbar.background = AppCompatResources.getDrawable(
context,
ThemeManager.resolveAttribute(R.attr.bottomBarBackgroundTop, context)
)
}
@ -64,7 +63,10 @@ class TabPreview @JvmOverloads constructor(
fun loadPreviewThumbnail(thumbnailId: String) {
doOnNextLayout {
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 index = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
currentIndex - 1
} else {
currentIndex + 1
} else {
currentIndex - 1
}
GestureDirection.LEFT_TO_RIGHT -> if (isLtr) {
currentIndex + 1
} else {
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.service.fxa.manager.FxaAccountManager
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.StrictModeManager
import org.mozilla.fenix.perf.StrictModeManager
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.MetricController
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
import org.mozilla.geckoview.BuildConfig.MOZ_APP_BUILDID
import org.mozilla.geckoview.BuildConfig.MOZ_APP_VENDOR
@ -36,7 +37,7 @@ import org.mozilla.geckoview.BuildConfig.MOZ_UPDATE_CHANNEL
class Analytics(
private val context: Context
) {
val crashReporter: CrashReporter by lazy {
val crashReporter: CrashReporter by lazyMonitored {
val services = mutableListOf<CrashReporterService>()
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(
listOf(
GleanMetricsService(context),

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

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

@ -9,6 +9,7 @@ import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.StrictMode
import androidx.core.content.ContextCompat
import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers
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.storage.SessionStorage
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.RestoreCompleteAction
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore
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.HomeActivity
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.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
@ -90,7 +92,7 @@ class Core(
* The browser engine component initialized based on the build
* configuration (see build variants).
*/
val engine: Engine by lazy {
val engine: Engine by lazyMonitored {
val defaultSettings = DefaultSettings(
requestInterceptor = AppRequestInterceptor(context),
remoteDebuggingEnabled = context.settings().isRemoteDebuggingEnabled &&
@ -103,7 +105,11 @@ class Core(
fontInflationEnabled = context.settings().shouldUseAutoSize,
suspendMediaWhenInactive = false,
forceUserScalableContent = context.settings().forceEnableZoom,
loginAutofillEnabled = context.settings().shouldAutofillLogins
loginAutofillEnabled = context.settings().shouldAutofillLogins,
clearColor = ContextCompat.getColor(
context,
R.color.foundation_normal_theme
)
)
GeckoEngine(
@ -132,7 +138,7 @@ class Core(
/**
* [Client] implementation to be used for code depending on `concept-fetch``
*/
val client: Client by lazy {
val client: Client by lazyMonitored {
GeckoViewFetchClient(
context,
GeckoProvider.getOrCreateRuntime(
@ -143,14 +149,14 @@ class Core(
)
}
private val sessionStorage: SessionStorage by lazy {
private val sessionStorage: SessionStorage by lazyMonitored {
SessionStorage(context, engine = engine)
}
/**
* The [BrowserStore] holds the global [BrowserState].
*/
val store by lazy {
val store by lazyMonitored {
BrowserStore(
middleware = listOf(
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
@ -181,12 +187,12 @@ class Core(
/**
* 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.
*/
val relationChecker: RelationChecker by lazy {
val relationChecker: RelationChecker by lazyMonitored {
StatementRelationChecker(StatementApi(client))
}
@ -196,7 +202,7 @@ class Core(
* sessions from the [SessionStorage], and with a default session (about:blank) in
* case all sessions/tabs are closed.
*/
val sessionManager by lazy {
val sessionManager by lazyMonitored {
SessionManager(engine, store).also { sessionManager ->
// Install the "icons" WebExtension to automatically load icons for every visited website.
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
if (!context.settings().manuallyCloseTabs) {
store.state.tabs.filter {
(System.currentTimeMillis() - it.lastAccess) > context.settings().getTabTimeout()
(System.currentTimeMillis() - it.lastAccess) > context.settings()
.getTabTimeout()
}.forEach {
val session = sessionManager.findSessionById(it.id)
if (session != null) {
@ -254,26 +261,26 @@ class Core(
/**
* Icons component for loading, caching and processing website icons.
*/
val icons by lazy {
val icons by lazyMonitored {
BrowserIcons(context, client)
}
val metrics by lazy {
val metrics by lazyMonitored {
context.components.analytics.metrics
}
val adsTelemetry by lazy {
val adsTelemetry by lazyMonitored {
AdsTelemetry(metrics)
}
val searchTelemetry by lazy {
val searchTelemetry by lazyMonitored {
InContentTelemetry(metrics)
}
/**
* Shortcut component for managing shortcuts on the device home screen.
*/
val webAppShortcutManager by lazy {
val webAppShortcutManager by lazyMonitored {
WebAppShortcutManager(
context,
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.
// For example, this is how the GeckoEngine delegates (history, logins) are configured.
// We can fully initialize GeckoEngine without initialized our storage.
val lazyHistoryStorage = lazy { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazy { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazy { SyncableLoginsStorage(context, passwordsEncryptionKey) }
val lazyHistoryStorage = lazyMonitored { PlacesHistoryStorage(context, crashReporter) }
val lazyBookmarksStorage = lazyMonitored { PlacesBookmarksStorage(context) }
val lazyPasswordsStorage = lazyMonitored { SyncableLoginsStorage(context, passwordsEncryptionKey) }
/**
* 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.
val historyStorage by lazy { lazyHistoryStorage.value }
val bookmarksStorage by lazy { lazyBookmarksStorage.value }
val passwordsStorage by lazy { lazyPasswordsStorage.value }
val historyStorage: PlacesHistoryStorage get() = lazyHistoryStorage.value
val bookmarksStorage: PlacesBookmarksStorage get() = lazyBookmarksStorage.value
val passwordsStorage: SyncableLoginsStorage get() = lazyPasswordsStorage.value
val tabCollectionStorage by lazy {
val tabCollectionStorage by lazyMonitored {
TabCollectionStorage(
context,
sessionManager,
@ -311,37 +318,53 @@ class Core(
/**
* 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>>()
strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
if (!context.settings().defaultTopSitesAdded) {
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_google),
SupportUtils.GOOGLE_URL
if (Config.channel.isMozillaOnline) {
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_baidu),
SupportUtils.BAIDU_URL
)
)
)
if (LocaleManager.getSelectedLocale(context).language == "en") {
defaultTopSites.add(
Pair(
context.getString(R.string.pocket_pinned_top_articles),
SupportUtils.POCKET_TRENDING_URL
context.getString(R.string.default_top_site_jd),
SupportUtils.JD_URL
)
)
}
} else {
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_google),
SupportUtils.GOOGLE_URL
)
)
if (LocaleManager.getSelectedLocale(context).language == "en") {
defaultTopSites.add(
Pair(
context.getString(R.string.pocket_pinned_top_articles),
SupportUtils.POCKET_TRENDING_URL
)
)
}
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_wikipedia),
SupportUtils.WIKIPEDIA_URL
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_wikipedia),
SupportUtils.WIKIPEDIA_URL
)
)
)
}
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+
@ -372,7 +395,7 @@ class Core(
forceInsecure = !Config.channel.isNightlyOrDebug
)
private val passwordsEncryptionKey by lazy {
private val passwordsEncryptionKey by lazyMonitored {
getSecureAbove22Preferences().getString(PASSWORDS_KEY)
?: generateEncryptionKey(KEY_STRENGTH).also {
if (context.settings().passwordsEncryptionKeyGenerated &&

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

@ -19,6 +19,7 @@ import mozilla.components.support.migration.MigrationIntentProcessor
import mozilla.components.support.migration.state.MigrationStore
import org.mozilla.fenix.customtabs.FennecWebAppIntentProcessor
import org.mozilla.fenix.home.intent.FennecBookmarkShortcutsIntentProcessor
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
/**
@ -39,26 +40,26 @@ class IntentProcessors(
/**
* 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)
}
/**
* 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)
}
val customTabIntentProcessor by lazy {
val customTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = false)
}
val privateCustomTabIntentProcessor by lazy {
val privateCustomTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = true)
}
val externalAppIntentProcessors by lazy {
val externalAppIntentProcessors by lazyMonitored {
listOf(
TrustedWebActivityIntentProcessor(
sessionManager = sessionManager,
@ -72,11 +73,11 @@ class IntentProcessors(
)
}
val fennecPageShortcutIntentProcessor by lazy {
val fennecPageShortcutIntentProcessor by lazyMonitored {
FennecBookmarkShortcutsIntentProcessor(sessionManager, sessionUseCases.loadUrl)
}
val migrationIntentProcessor by lazy {
val migrationIntentProcessor by lazyMonitored {
MigrationIntentProcessor(migrationStore)
}
}

@ -6,10 +6,11 @@ package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.VisualCompletenessQueue
import org.mozilla.fenix.perf.lazyMonitored
/**
* Component group for all functionality related to performance.
*/
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.support.base.log.logger.Logger
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.push.FirebasePushService
/**
@ -17,7 +18,7 @@ import org.mozilla.fenix.push.FirebasePushService
* push messaging (e.g. WebPush, SendTab).
*/
class Push(context: Context, crashReporter: CrashReporter) {
val feature by lazy {
val feature by lazyMonitored {
pushConfig?.let { config ->
AutoPushFeature(
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 projectIdKey = context.getString(R.string.pref_key_push_project_id)
val resId = context.resources.getIdentifier(projectIdKey, "string", context.packageName)
if (resId == 0) {
logger.warn("No firebase configuration found; cannot support push service.")
return@lazy null
return@lazyMonitored null
}
logger.debug("Creating push configuration for autopush.")
@ -42,5 +43,5 @@ class Push(context: Context, crashReporter: CrashReporter) {
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 mozilla.components.browser.search.SearchEngineManager
import org.mozilla.fenix.components.searchengine.FenixSearchEngineProvider
import org.mozilla.fenix.perf.lazyMonitored
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.
*/
val searchEngineManager by lazy {
val searchEngineManager by lazyMonitored {
SearchEngineManager(
coroutineContext = IO,
providers = listOf(provider)

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

@ -19,7 +19,7 @@ import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage
import mozilla.components.support.base.observer.Observable
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.toShortUrl
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.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
/**
@ -42,19 +43,18 @@ class UseCases(
/**
* 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.
*/
val tabsUseCases: TabsUseCases by lazy { TabsUseCases(store, sessionManager) }
val tabsUseCases: TabsUseCases by lazyMonitored { TabsUseCases(store, sessionManager) }
/**
* Use cases that provide search engine integration.
*/
val searchUseCases by lazy {
val searchUseCases by lazyMonitored {
SearchUseCases(
context,
store,
searchEngineManager.toDefaultSearchEngineProvider(context),
sessionManager
@ -64,22 +64,22 @@ class UseCases(
/**
* 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)
}
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.
*/
val topSitesUseCase by lazy { TopSitesUseCases(topSitesStorage) }
val topSitesUseCase by lazyMonitored { TopSitesUseCases(topSitesStorage) }
}

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

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

@ -52,10 +52,6 @@ sealed class Event {
object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event()
object UriOpened : Event()
object QRScannerOpened : Event()
object QRScannerPromptDisplayed : Event()
object QRScannerNavigationAllowed : Event()
object QRScannerNavigationDenied : Event()
object SyncAuthOpened : Event()
object SyncAuthClosed : Event()
object SyncAuthSignUp : Event()
@ -70,7 +66,6 @@ sealed class Event {
object SyncAuthFromSharedReuse : Event()
object SyncAuthFromSharedCopy : Event()
object SyncAccountOpened : Event()
object SyncAccountClosed : Event()
object SyncAccountSyncNow : Event()
object SendTab : Event()
object SignInToSendTab : Event()
@ -98,11 +93,8 @@ sealed class Event {
object FindInPageOpened : Event()
object FindInPageClosed : Event()
object FindInPageSearchCommitted : Event()
object PrivateBrowsingGarbageIconTapped : Event()
object PrivateBrowsingSnackbarUndoTapped : Event()
object PrivateBrowsingNotificationTapped : Event()
object PrivateBrowsingNotificationOpenTapped : Event()
object PrivateBrowsingNotificationDeleteAndOpenTapped : Event()
object PrivateBrowsingCreateShortcut : Event()
object PrivateBrowsingAddShortcutCFR : Event()
object PrivateBrowsingCancelCFR : Event()
@ -152,16 +144,11 @@ sealed class Event {
object FennecToFenixMigrated : Event()
object AddonsOpenInSettings : Event()
object VoiceSearchTapped : Event()
object SearchWidgetCFRDisplayed : Event()
object SearchWidgetCFRCanceled : Event()
object SearchWidgetCFRNotNowPressed : Event()
object SearchWidgetCFRAddWidgetPressed : Event()
object SearchWidgetInstalled : Event()
object OnboardingAutoSignIn : Event()
object OnboardingManualSignIn : Event()
object OnboardingPrivacyNotice : Event()
object OnboardingPrivateBrowsing : Event()
object OnboardingWhatsNew : Event()
object OnboardingFinish : Event()
object ChangedToDefaultBrowser : Event()
@ -508,7 +495,8 @@ sealed class Event {
"mozac.feature.contextmenu.save_image" to "save_image",
"mozac.feature.contextmenu.share_link" to "share_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.private.NoExtraKeys
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AppTheme
@ -38,13 +39,11 @@ import org.mozilla.fenix.GleanMetrics.Preferences
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingMode
import org.mozilla.fenix.GleanMetrics.PrivateBrowsingShortcut
import org.mozilla.fenix.GleanMetrics.ProgressiveWebApp
import org.mozilla.fenix.GleanMetrics.QrScanner
import org.mozilla.fenix.GleanMetrics.ReaderMode
import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.SearchSuggestions
import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SearchWidgetCfr
import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab
@ -230,18 +229,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.UriOpened -> EventWrapper<NoExtraKeys>(
{ 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(
{ ErrorPage.visitedError.record(it) },
{ ErrorPage.visitedErrorKeys.valueOf(it) }
@ -270,9 +257,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAuthOtherExternal -> EventWrapper<NoExtraKeys>(
{ SyncAuth.otherExternal.record(it) }
)
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> EventWrapper<NoExtraKeys>(
{ SyncAuth.autoLogin.record(it) }
)
is Event.SyncAuthRecovered -> EventWrapper<NoExtraKeys>(
{ SyncAuth.recovered.record(it) }
)
@ -285,9 +269,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SyncAccountOpened -> EventWrapper<NoExtraKeys>(
{ SyncAccount.opened.record(it) }
)
is Event.SyncAccountClosed -> EventWrapper<NoExtraKeys>(
{ SyncAccount.closed.record(it) }
)
is Event.SyncAccountSyncNow -> EventWrapper<NoExtraKeys>(
{ SyncAccount.syncNow.record(it) }
)
@ -376,21 +357,12 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SearchWidgetVoiceSearchPressed -> EventWrapper<NoExtraKeys>(
{ SearchWidget.voiceButton.record(it) }
)
is Event.PrivateBrowsingGarbageIconTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.garbageIcon.record(it) }
)
is Event.PrivateBrowsingSnackbarUndoTapped -> EventWrapper<NoExtraKeys>(
{ PrivateBrowsingMode.snackbarUndo.record(it) }
)
is Event.PrivateBrowsingNotificationTapped -> EventWrapper<NoExtraKeys>(
{ 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>(
{ PrivateBrowsingShortcut.createShortcut.record(it) }
)
@ -579,25 +551,10 @@ private val Event.wrapper: EventWrapper<*>?
is Event.VoiceSearchTapped -> EventWrapper<NoExtraKeys>(
{ 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(
{ Events.tabCounterMenuAction.record(it) },
{ Events.tabCounterMenuActionKeys.valueOf(it) }
)
is Event.OnboardingWhatsNew -> EventWrapper<NoExtraKeys>(
{ Onboarding.whatsNew.record(it) }
)
is Event.OnboardingPrivateBrowsing -> EventWrapper<NoExtraKeys>(
{ Onboarding.prefToggledPrivateBrowsing.record(it) }
)
@ -721,6 +678,7 @@ private val Event.wrapper: EventWrapper<*>?
is Event.AddonInstalled -> null
is Event.SearchWidgetInstalled -> null
is Event.ChangedToDefaultBrowser -> null
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> null
}
class GleanMetricsService(
@ -767,6 +725,14 @@ class GleanMetricsService(
mozillaProductDetector.getMozillaBrowserDefault(context)?.also {
defaultMozBrowser.set(it)
}
distributionId.set(
when (Config.channel.isMozillaOnline) {
true -> "MozillaOnline"
false -> "Mozilla"
}
)
mozillaProducts.set(mozillaProductDetector.getInstalledMozillaProducts(context))
adjustCampaign.set(context.settings().adjustCampaignId)

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

@ -17,6 +17,8 @@ import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionFeature
@ -63,10 +65,11 @@ class DefaultBrowserToolbarMenuController(
private val swipeRefresh: SwipeRefreshLayout,
private val customTabSession: Session?,
private val openInFenixIntent: Intent,
private val bookmarkTapped: (Session) -> Unit,
private val bookmarkTapped: (String, String) -> Unit,
private val scope: CoroutineScope,
private val tabCollectionStorage: TabCollectionStorage,
private val topSitesStorage: DefaultTopSitesStorage
private val topSitesStorage: DefaultTopSitesStorage,
private val browserStore: BrowserStore
) : BrowserToolbarMenuController {
private val currentSession
@ -184,7 +187,7 @@ class DefaultBrowserToolbarMenuController(
val directions = NavGraphDirections.actionGlobalShareFragment(
data = arrayOf(
ShareData(
url = currentSession?.url,
url = getProperUrl(currentSession),
title = currentSession?.title
)
),
@ -270,7 +273,7 @@ class DefaultBrowserToolbarMenuController(
}
ToolbarMenu.Item.Bookmark -> {
sessionManager.selectedSession?.let {
bookmarkTapped(it)
getProperUrl(it)?.let { url -> bookmarkTapped(url, it.title) }
}
}
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")
private fun trackToolbarItemInteraction(item: ToolbarMenu.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.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.state.ExternalAppType
@ -49,6 +50,7 @@ interface BrowserToolbarViewInteractor {
fun onReaderModePressed(enabled: Boolean)
}
@ExperimentalCoroutinesApi
@SuppressWarnings("LargeClass")
class BrowserToolbarView(
private val container: ViewGroup,

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

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

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

@ -11,6 +11,8 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
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.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.res.resolveAttribute
@ -24,7 +26,6 @@ import java.lang.ref.WeakReference
@OptIn(ExperimentalCoroutinesApi::class)
class TabCounterToolbarButton(
private val lifecycleOwner: LifecycleOwner,
private val isPrivate: Boolean,
private val onItemTapped: (TabCounterMenu.Item) -> Unit = {},
private val showTabs: () -> Unit
) : Toolbar.Action {
@ -37,7 +38,7 @@ class TabCounterToolbarButton(
val settings = parent.context.components.settings
store.flowScoped(lifecycleOwner) { flow ->
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate).size }
flow.map { state -> state.getNormalOrPrivateTabs(isPrivate(store)).size }
.ifChanged()
.collect { tabs -> updateCount(tabs) }
}
@ -58,7 +59,7 @@ class TabCounterToolbarButton(
addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
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 */ }
@ -77,4 +78,8 @@ class TabCounterToolbarButton(
private fun updateCount(count: Int) {
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 androidx.appcompat.content.res.AppCompatResources
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.DomainAutocompleteProvider
import mozilla.components.browser.toolbar.BrowserToolbar
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.theme.ThemeManager
@ExperimentalCoroutinesApi
abstract class ToolbarIntegration(
context: Context,
toolbar: BrowserToolbar,
@ -45,7 +47,7 @@ abstract class ToolbarIntegration(
)
private val menuPresenter =
MenuPresenter(toolbar, context.components.core.sessionManager, sessionId)
MenuPresenter(toolbar, context.components.core.store, sessionId)
init {
toolbar.display.menuBuilder = toolbarMenu.menuBuilder
@ -67,6 +69,7 @@ abstract class ToolbarIntegration(
}
}
@ExperimentalCoroutinesApi
class DefaultToolbarIntegration(
context: Context,
toolbar: BrowserToolbar,
@ -138,7 +141,6 @@ class DefaultToolbarIntegration(
val tabsAction = TabCounterToolbarButton(
lifecycleOwner,
isPrivate,
onItemTapped = {
interactor.onTabCounterMenuItemTapped(it)
},

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

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

@ -9,7 +9,6 @@ import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.CustomTabConfig
@ -30,6 +29,7 @@ import mozilla.components.support.utils.toSafeIntent
import org.json.JSONException
import org.json.JSONObject
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.File
import java.io.IOException
@ -61,7 +61,7 @@ class FennecWebAppIntentProcessor(
val url = safeIntent.dataString
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)
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 androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import mozilla.components.concept.fetch.Client
import mozilla.components.concept.fetch.Request
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.lib.publicsuffixlist.ext.urlToTrimmedHost
import mozilla.components.support.ktx.android.net.hostWithoutCommonPrefixes
import org.mozilla.fenix.perf.runBlockingIncrement
import java.io.IOException
import java.net.IDN
import java.util.Locale
@ -92,9 +92,10 @@ private fun Uri.isIpv6(): Boolean {
/**
* Trim a host's prefix and suffix
*/
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String = runBlocking {
urlToTrimmedHost(publicSuffixList).await()
}
fun String.urlToTrimmedHost(publicSuffixList: PublicSuffixList): String =
runBlockingIncrement {
urlToTrimmedHost(publicSuffixList).await()
}
/**
* Trims a URL string of its scheme and common prefixes.

@ -2,12 +2,15 @@
* 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:Suppress("TooManyFunctions")
package org.mozilla.fenix.ext
import android.graphics.Rect
import android.os.Build
import android.view.TouchDelegate
import android.view.View
import android.view.accessibility.AccessibilityNodeInfo
import androidx.annotation.Dimension
import androidx.annotation.Dimension.DP
import androidx.annotation.VisibleForTesting
@ -33,18 +36,86 @@ 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.
*
* @see View.getLocationOnScreen
* @see View.getRectWithViewLocation for a version of this that is relative to a window
*/
*/
fun View.getRectWithScreenLocation(): Rect {
val locationOnScreen = IntArray(2).apply { getLocationOnScreen(this) }
return Rect(locationOnScreen[0],
return Rect(
locationOnScreen[0],
locationOnScreen[1],
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