Compare commits

...

No commits in common. 'fork' and 'iceraven-2.17.2' have entirely different histories.

@ -0,0 +1,154 @@
projects:
app:
upstream_dependencies:
- browser-domains
- browser-engine-gecko
- browser-errorpages
- browser-icons
- browser-menu
- browser-menu2
- browser-session-storage
- browser-state
- browser-storage-sync
- browser-tabstray
- browser-thumbnails
- browser-toolbar
- compose-awesomebar
- compose-cfr
- concept-awesomebar
- concept-base
- concept-engine
- concept-fetch
- concept-menu
- concept-push
- concept-storage
- concept-sync
- concept-tabstray
- concept-toolbar
- feature-accounts
- feature-accounts-push
- feature-addons
- feature-app-links
- feature-autofill
- feature-awesomebar
- feature-contextmenu
- feature-customtabs
- feature-downloads
- feature-findinpage
- feature-fxsuggest
- feature-intent
- feature-logins
- feature-media
- feature-privatemode
- feature-prompts
- feature-push
- feature-pwa
- feature-qr
- feature-readerview
- feature-recentlyclosed
- feature-search
- feature-session
- feature-share
- feature-sitepermissions
- feature-syncedtabs
- feature-tab-collections
- feature-tabs
- feature-toolbar
- feature-top-sites
- feature-webauthn
- feature-webcompat
- feature-webcompat-reporter
- feature-webnotifications
- lib-crash
- lib-crash-sentry
- lib-dataprotect
- lib-publicsuffixlist
- lib-push-firebase
- lib-state
- service-contile
- service-digitalassetlinks
- service-firefox-accounts
- service-glean
- service-location
- service-nimbus
- service-pocket
- service-sync-autofill
- service-sync-logins
- support-base
- support-images
- support-ktx
- support-locale
- support-remotesettings
- support-rusterrors
- support-rusthttp
- support-rustlog
- support-test
- support-test-libstate
- support-utils
- support-webextensions
- ui-autocomplete
- ui-colors
- ui-icons
- ui-tabcounter
- ui-widgets
variants:
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-debug.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-debug.apk
- abi: x86
fileName: app-fenix-x86-debug.apk
- abi: x86_64
fileName: app-fenix-x86_64-debug.apk
build_type: debug
name: fenixDebug
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-release-unsigned.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-release-unsigned.apk
- abi: x86
fileName: app-fenix-x86-release-unsigned.apk
- abi: x86_64
fileName: app-fenix-x86_64-release-unsigned.apk
build_type: release
name: fenixRelease
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-nightly-unsigned.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-nightly-unsigned.apk
- abi: x86
fileName: app-fenix-x86-nightly-unsigned.apk
- abi: x86_64
fileName: app-fenix-x86_64-nightly-unsigned.apk
build_type: nightly
name: fenixNightly
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-beta-unsigned.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-beta-unsigned.apk
- abi: x86
fileName: app-fenix-x86-beta-unsigned.apk
- abi: x86_64
fileName: app-fenix-x86_64-beta-unsigned.apk
build_type: beta
name: fenixBeta
- apks:
- abi: arm64-v8a
fileName: app-fenix-arm64-v8a-benchmark-unsigned.apk
- abi: armeabi-v7a
fileName: app-fenix-armeabi-v7a-benchmark-unsigned.apk
- abi: x86
fileName: app-fenix-x86-benchmark-unsigned.apk
- abi: x86_64
fileName: app-fenix-x86_64-benchmark-unsigned.apk
build_type: benchmark
name: fenixBenchmark
- apks:
- abi: noarch
fileName: app-debug-androidTest.apk
build_type: androidTest
name: androidTest

@ -1,41 +0,0 @@
# Definitions for jobs that run periodically. For details on the format, see
# `taskcluster/taskgraph/cron/schema.py`. For documentation, see
# `taskcluster/docs/cron.rst`.
---
jobs:
- name: nightly
job:
type: decision-task
treeherder-symbol: Nd
target-tasks-method: nightly
when:
- {hour: 5, minute: 0}
- {hour: 17, minute: 0}
- name: nightly-test
job:
type: decision-task
treeherder-symbol: Nt
target-tasks-method: nightly-test
when:
- {hour: 5, minute: 0}
- name: fennec-production
job:
type: decision-task
treeherder-symbol: fennec-production
target-tasks-method: fennec-production
when: [] # Force hook only
- name: screenshots
job:
type: decision-task
treeherder-symbol: screenshots-D
target-tasks-method: screenshots
when: [{weekday: 'Monday', hour: 10, minute: 0}]
- name: legacy-api-ui-tests
job:
type: decision-task
treeherder-symbol: legacy-api-ui
target-tasks-method: legacy_api_ui_tests
when:
- {hour: 11, minute: 0}
- {hour: 20, minute: 0}

@ -1,108 +0,0 @@
---
cookie-banners:
description: Features for cookie banner handling.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
growth-data:
description: A feature measuring campaign growth data
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active"
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
messaging:
description: "Configuration for the messaging system.\n\nIn practice this is a set of growable lookup tables for the\nmessage controller to piece together.\n"
hasExposure: true
exposureDescription: ""
variables:
actions:
type: json
description: A growable map of action URLs.
message-under-experiment:
type: string
description: Id or prefix of the message under experiment.
messages:
type: json
description: A growable collection of messages
notification-config:
type: json
description: Configuration of the notification worker for all notification messages.
on-control:
type: string
description: What should be displayed when a control message is selected.
enum:
- show-next-message
- show-none
styles:
type: json
description: "A map of styles to configure message appearance.\n"
triggers:
type: json
description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
mr2022:
description: Features for MR 2022.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
nimbus-validation:
description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.
hasExposure: true
exposureDescription: ""
variables:
settings-icon:
type: string
description: The drawable displayed in the app menu for Settings
settings-punctuation:
type: string
description: The emoji displayed in the Settings screen title.
settings-title:
type: string
description: The title of displayed in the Settings screen and app menu.
pre-permission-notification-prompt:
description: A feature that shows the pre-permission notification prompt.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, the pre-permission notification prompt is shown to the user."
re-engagement-notification:
description: A feature that shows the re-enagement notification if the user is inactive.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the re-engagement notification is shown to the inactive user."
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."
unified-search:
description: A feature allowing user to easily search for specified results directly in the search bar.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature shows up in the search bar."

@ -1,5 +0,0 @@
# .git-blame-ignore-revs
# For #27667 - Remove import-ordering from the list of disabled ktlint rules (#27680)
9654b4dfb122b54b04369fe80a2f9c95811478e8
# For #26844: Fix ktlint issues and remove them from baseline. (#26901)
ffcef5ff2e3f78b6972dd16551f3f653b7035ccc

31
.github/CODEOWNERS vendored

@ -1,31 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This CODEOWNERS file defines individuals or teams that are responsible
# for code in this repository. Code owners are automatically requested
# for review when someone opens a pull request that modifies code that
# they own. Order is important; the last matching pattern takes the most
# precedence.
# A CODEOWNERS file uses a pattern that follows the same rules used in
# gitignore files. The pattern is followed by one or more GitHub usernames
# or team names using the standard @username or @org/team-name format.
# You can also refer to a user by an email address that has been added
# to their GitHub account, for example user@example.com.
# https://help.github.com/articles/about-codeowners/
# WARNING: if there is a single syntax error in this file, CODEOWNERS
# WILL NOT WORK AT ALL. Please be careful when editing this file.
#
# You can use the technique described in this blog post to validate
# the paths you specify in .gitignore:
# http://www.benjaminoakes.com/git/2018/08/10/Testing-changes-to-GitHub-CODEOWNERS/
# By default the Android Components team will be the owner for everything in
# the repo. Unless a later match takes precedence.
* @mozilla-mobile/ACT @mozilla-mobile/fenix
/.cron.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/.taskcluster.yml @mozilla-mobile/releng @mozilla-mobile/fenix
/automation/ @mozilla-mobile/fenix
/taskcluster/ @mozilla-mobile/releng @mozilla-mobile/fenix
/.github/ @mozilla-mobile/fenix

@ -1,91 +0,0 @@
name: "\U0001F41E Bug report"
description: Create a report to help us improve.
title: "[Bug]: "
labels: ["\U0001F41E bug", "needs:triage"]
body:
- type: markdown
attributes:
value: |
- Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean.
- Have a look at ["I want to file an issue!"][info] for more information.
- Read the [Community Participation Guidelines][guidelines] and the [Bugzilla Etiquette guidelines][bugzilla] before filing an issue.
[info]: https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue
[guidelines]: https://www.mozilla.org/en-US/about/governance/policies/participation/
[bugzilla]: https://bugzilla.mozilla.org/page.cgi?id=etiquette.html
- type: textarea
attributes:
label: Steps to reproduce
description: Steps to reproduce the behaviour.
placeholder: |
1. Have a tab open..
2. Go to..
3. Click on..
4. Observe..
validations:
required: true
- type: textarea
attributes:
label: Expected behaviour
placeholder: A menu should open..
validations:
required: true
- type: textarea
attributes:
label: Actual behaviour
placeholder: The app closes unexpectedly..
validations:
required: true
- type: markdown
attributes:
value: |
# Device information
- type: input
attributes:
label: Device name
description: The name of the device model and manufacturer.
placeholder: Google Pixel 2
validations:
required: false
- type: input
attributes:
label: Android version
description: You can find the Android version information in the About section of your device's system settings.
placeholder: Android 10
validations:
required: true
- type: dropdown
attributes:
label: Firefox release type
description: You can find this information in Settings -> About Firefox.
options:
- Firefox Nightly
- Firefox Beta
- Firefox
validations:
required: true
- type: input
attributes:
label: Firefox version
description: You can find this information in Settings -> About Firefox.
placeholder: 89.0.10
validations:
required: true
- type: textarea
attributes:
label: Device logs
description: |
Device logs or crash information can greatly aid in debugging. You can find some details here on how to [retrieve device logs or crash IDs][log].
[log]: https://github.com/mozilla-mobile/fenix/wiki/Logging-Crash-Information
validations:
required: false
- type: textarea
attributes:
label: Additional information
description: |
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here, by
dragging and dropping files in the field below.
validations:
required: false

@ -1,19 +0,0 @@
---
name: "⭐️ Feature request"
about: Suggest an idea for this project
title: ''
labels: "🌟 feature request"
assignees: ''
---
[comment]: # (Please do your best to search for duplicate issues before filing a new issue so we can keep our issue board clean)
[comment]: # (Every issue should have exactly one feature request described in it. Please do not file feedback list tickets as it is difficult to parse them and address their individual points)
[comment]: # (Feature Requests are better when theyre open-ended instead of demanding a specific solution e.g: “I want an easier way to do X” instead of “add Y”)
[comment]: # (Read https://github.com/mozilla-mobile/fenix#i-want-to-file-an-issue for more information)
### What is the user problem or growth opportunity you want to see solved?
### How do you know that this problem exists today? Why is this important?
### Who will benefit from it?

@ -1,19 +0,0 @@
---
name: "⌛ Performance issue"
about: Create a performance issue if the app is slow or it uses too much memory, disk space, battery, or network data
title: ""
labels: "performance"
assignees: ''
---
## Steps to reproduce
### Expected behavior
### Actual behavior
### Device information
* Android device: ?
* Fenix version: ?

@ -1,25 +0,0 @@
---
name: "\U0001F4BB Web content issue report"
about: Create an issue specifically about something wrong with web content while using Fenix
title: "[webcontent]"
labels: "\U0001F4BB bug"
assignees: ''
---
## Site with issue and any steps to reproduce
### Expected behavior
### Actual behavior
### Does toggling Tracking Protection fix the issue? (Press the shield icon in the toolbar while on the site to see toggle)
### Can you reproduce in Chrome (or other non-Mozilla browser)?
<!--- Note: If you can reproduce the same issue in Firefox for Android AND Chrome, this issue is very unlikely to be fixed by our teams. -->
<!-- If you cannot reproduce in Chrome and know how to do so, please consider filing a Bugzilla issue here for faster triage https://bugzilla.mozilla.org/enter_bug.cgi?product=GeckoView&component=General -->
### Device information
* Android device: ?
* Fenix version: ?

@ -1,24 +0,0 @@
---
name: "\U000026CF Investigative Spike"
about: Create an investigation spike
title: "[Spike]"
---
## Title
Brief description of what needs to be investigated, including the User story for which the spike is needed.
## Description
Description of what is being investigated, including:
Method of investigation (engineering research, prototype, etc.
Boundaries of investigation (time box to x hours, does not include UX, etc.)
## Deliverables
Description of deliverables, including:
Documentation of investigation results (within the spike ticket, or linked to it), including:
Findings
Recommendations
List of possible user stories to implement recommendations, including estimates
Next Steps
Reach out to Product to go over results of investigation.

@ -1,21 +0,0 @@
---
name: "\U0001F4C8 Telemetry User Story"
about: Create a telemetry user story, assigned to an Epic to track telemetry
title: "[Telemetry]"
labels: Feature:Telemetry
assignees: ''
---
## Description & Product Manager / Data Scientist User Story
## What questions will you answer with this data?
## Acceptance Criteria
- [ ] ENG files a [DS JIRA](https://jira.mozilla.com/projects/DO/issues/DO-228?filter=allopenissues) request outlining their methodology.
- [ ] DS sign off on instrumentation methodology addressing product questions.
- [ ] Event pings can be queried via re:dash
- [ ] Event pings can be queried via amplitude
- [ ] We are sending telemetry events for the actions listed in the requirements
- [ ] We have documented the telemetry
- [ ] We have asked a data steward to [review](https://github.com/mozilla/data-review/blob/master/request.md) the telemetry

@ -1,13 +0,0 @@
---
name: "\U0001F494 Intermittent UI Test Issue"
about: Create an issue to help log a UI test failure
labels: "eng:ui-test, intermittent-test"
title: "Intermittent UI test failure - <Classname.testName>"
assignees: ''
---
### Firebase Test Run:
Provide a Firebase test run report link here showcasing the problem
### Stacktrace:
### Build:

@ -1,13 +0,0 @@
---
name: "\U0001F6A8 Intermittent Unit Test Issue"
about: Create an issue to help log a Unit Test failure
labels: "eng:intermittent-test"
title: "Intermittent Unit Test failure - <Classname.testName>"
assignees: ''
---
### Test Run:
Provide a test run report link here showcasing the problem (e.g, Taskcluster), and a link to the source Github event
### Stacktrace:
### Build:

@ -1,18 +0,0 @@
---
name: "\U0001F469\U0001F3FB\U0001F4BB User Story"
about: Create a user story for an epic
title: ""
labels: ''
assignees: ''
---
## User Story
- As a user, I want … so I can do … (keep it problem-centric)
## Dependencies
- List dependencies on other issues/teams etc.
### Acceptance Criteria
- I can do … (e.g. add a bookmark)

@ -1,14 +0,0 @@
### Pull Request checklist
<!-- Before submitting the PR, please address each item -->
- [ ] **Tests**: This PR includes thorough tests or an explanation of why it does not
- [ ] **Screenshots**: This PR includes screenshots or GIFs of the changes made or an explanation of why it does not
- [ ] **Accessibility**: The code in this PR follows [accessibility best practices](https://github.com/mozilla-mobile/shared-docs/blob/master/android/accessibility_guide.md) or does not include any user facing features. In addition, it includes a screenshot of a successful [accessibility scan](https://play.google.com/store/apps/details?id=com.google.android.apps.accessibility.auditor&hl=en_US) to ensure no new defects are added to the product.
### To download an APK when reviewing a PR:
The PR runs an Android build check (`run-build`) that builds a `forkRelease` variant of the app. If it succeeds, then we upload the apks (signed with debug keys) via Github actions. We also generate a comment with some instructions and a link to help you find the downloads. You can also follow the instructions below:
1. Click Details next to "run-build (pull_request_target)" after it finishes with a green checkmark.
2. Click the "Artifacts" drop-down near the top right of the page.
3. The apk links should be present in the drop-down menu. You can click on the suitable CPU architecture to download a zipped apk file.
4. Unzip the file and install the apk.

@ -1,115 +0,0 @@
name: Android build PR
on:
pull_request:
branches:
- fork
jobs:
run-build:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Build forkRelease variant of app
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: app:assembleForkRelease -PversionName=${{ env.VERSION_NAME }}
run-testDebug:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run tests
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: testDebug
run-detekt:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run detekt
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: detekt
- name: Archive detekt results
uses: actions/upload-artifact@v2
with:
name: detekt report
path: build/reports/detekt.html
run-ktlint:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run ktlint
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: ktlint
run-lintDebug:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run lintDebug
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: lintDebug
- name: Archive lint results
uses: actions/upload-artifact@v2
with:
name: lintDebug report
path: app/build/reports/lint-results-debug.html

@ -1,143 +0,0 @@
name: Android build
on:
push:
branches:
- fork
jobs:
run-build:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Build forkRelease variant of app
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: app:assembleForkRelease -PversionName=${{ env.VERSION_NAME }}
- name: Create signed APKs
uses: abhijitvalluri/sign-apks@v0.8
with:
releaseDirectory: app/build/outputs/apk/forkRelease/
signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEY }}
alias: ${{ secrets.DEBUG_ALIAS }}
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
- name: Archive arm64 apk
uses: actions/upload-artifact@v2
with:
name: app-arm64-v8a-forkRelease.apk
path: app/build/outputs/apk/forkRelease/app-arm64-v8a-forkRelease.apk
- name: Archive armeabi apk
uses: actions/upload-artifact@v2
with:
name: app-armeabi-v7a-forkRelease.apk
path: app/build/outputs/apk/forkRelease/app-armeabi-v7a-forkRelease.apk
- name: Archive x86 apk
uses: actions/upload-artifact@v2
with:
name: app-x86-forkRelease.apk
path: app/build/outputs/apk/forkRelease/app-x86-forkRelease.apk
- name: Archive x86_64 apk
uses: actions/upload-artifact@v2
with:
name: app-x86_64-forkRelease.apk
path: app/build/outputs/apk/forkRelease/app-x86_64-forkRelease.apk
run-testDebug:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run tests
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: testDebug
run-detekt:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run detekt
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: detekt
- name: Archive detekt results
uses: actions/upload-artifact@v2
with:
name: detekt report
path: build/reports/detekt.html
run-ktlint:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run ktlint
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: ktlint
run-lintDebug:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 11
- name: Run lintDebug
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: lintDebug
- name: Archive lint results
uses: actions/upload-artifact@v2
with:
name: lintDebug report
path: app/build/reports/lint-results-debug.html

@ -1,24 +0,0 @@
name: PR comment
on:
pull_request_target:
types: [opened]
branches:
- fork
jobs: # Disabled because we cannot build changes from fork PRs using this repo's secrets due to Github limitations. So, the built apk will be from wrong code, so this is pointless.
comment-on-pr:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.pull_request.title), '[skip ci]')"
steps:
- name: Comment on PR with link to checks page
uses: mshick/add-pr-comment@v1
with:
message: |
### Download the built apks
You can download the apks built by Github actions **after** the CI checks pass.
Please go to the <a href="${{ github.event.pull_request.html_url }}/checks">checks page for this PR</a> to find the zipped apk files under the artifacts drop-down, as seen in the example screenshot below.
Note that you will have to click on the "Android build PR" tab on the left side to see the artifacts.
<img src="https://raw.githubusercontent.com/fork-maintainers/iceraven-browser/fork/.github/imgs/download-artifacts-screenshot.png" />
repo-token: ${{ secrets.GITHUB_TOKEN }}
allow-repeats: false

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

63
.github/stale.yml vendored

@ -1,63 +0,0 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 180
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 7
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
onlyLabels: []
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- pin
- "feature request 🌟"
- "eng:disabled-test"
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: false
# Set to true to ignore issues with an assignee (defaults to false)
exemptAssignees: false
# Label to use when marking as stale
staleLabel: wontfix
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
See: https://github.com/mozilla-mobile/fenix/issues/17373
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when removing the stale label.
# unmarkComment: >
# Your comment here.
# Comment to post when closing a stale Issue or Pull Request.
# closeComment: >
# Your comment here.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
# Limit to only `issues` or `pulls`
only: issues
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
# pulls:
# daysUntilStale: 30
# markComment: >
# This pull request has been automatically marked as stale because it has not had
# recent activity. It will be closed if no further activity occurs. Thank you
# for your contributions.
issues:
exemptLabels:
- pin
- "feature request 🌟"

@ -0,0 +1,96 @@
name: CI
on:
push:
branches:
- iceraven
jobs:
release-automation:
name: Build App
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: 'true'
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: 17
distribution: temurin
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Inspect memory
run: free -h
- name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Patch on the fly
run: |
sed -i 's#\.\./version.txt#\./version.txt#g' android-components/plugins/config/src/main/java/ConfigPlugin.kt
./automation/iceraven/patch_android_components.sh
- name: Relpace strings
run: |
sed -i 's/Firefox/Iceraven/g' app/src/*/res/values*/*strings.xml
sed -i '/about_content/s/Mozilla/@forkmaintainers/' app/src/*/res/values*/*strings.xml
- name: Build forkRelease variant of app
uses: gradle/gradle-build-action@v2
env:
# Try to stop the daemon from magically vanishing by adding random memory-related arguments.
# See <https://stackoverflow.com/a/70010526> and <https://stackoverflow.com/a/70756876>
# The runner seems to have ~6 gigs of memory, so we make sure to stay under that.
# We have Java 11 so we don't have a perm size anymore.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-XX:MaxMetaspaceSize=2g -Xms1g -Xmx3g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dev/stderr"
with:
gradle-home-cache-cleanup: true
gradle-executable: /usr/bin/time
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
- name: Setup build tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
- name: Create signed APKs
uses: abhijitvalluri/sign-apks@v0.8
with:
releaseDirectory: app/build/outputs/apk/fenix/forkRelease/
signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEY }}
alias: ${{ secrets.DEBUG_ALIAS }}
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Upload arm64 apk
uses: actions/upload-artifact@v3
with:
path: app/build/outputs/apk/fenix/forkRelease/app-fenix-arm64-v8a-forkRelease.apk
name: ${{ env.VERSION_NAME }}-browser-arm64-v8a-forkRelease.apk
- name: Upload armeabi apk
uses: actions/upload-artifact@v3
with:
path: app/build/outputs/apk/fenix/forkRelease/app-fenix-armeabi-v7a-forkRelease.apk
name: ${{ env.VERSION_NAME }}-browser-armeabi-v7a-forkRelease.apk
- name: Upload x86 apk
uses: actions/upload-artifact@v3
with:
path: app/build/outputs/apk/fenix/forkRelease/app-fenix-x86-forkRelease.apk
name: ${{ env.VERSION_NAME }}-browser-x86-forkRelease.apk
- name: Upload x86_64 apk
uses: actions/upload-artifact@v3
with:
path: app/build/outputs/apk/fenix/forkRelease/app-fenix-x86_64-forkRelease.apk
name: ${{ env.VERSION_NAME }}-browser-x86_64-forkRelease.apk

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/
name: "Sync Strings"
name: "Fenix - Sync Strings"
on:
schedule:

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/
name: "Update Android-Components"
name: "Fenix - Update Android-Components"
on:
schedule:

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/
name: "Update Nimbus Experiments"
name: "Fenix - Update Nimbus Experiments"
on:
schedule:
@ -15,14 +15,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: "Checkout Main Branch"
uses: actions/checkout@v2
uses: actions/checkout@v3
with:
path: fenix
ref: main
fetch-depth: 0
- name: "Update Experiments JSON"
id: update-experiments-json
uses: mozilla-mobile/update-experiments@v2
uses: mozilla-mobile/update-experiments@v3
with:
repo-path: fenix
output-path: app/src/main/res/raw/initial_experiments.json
@ -30,7 +30,7 @@ jobs:
app-name: fenix
branch: automation/update-nimbus-experiments
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
uses: peter-evans/create-pull-request@v4
if: steps.update-experiments-json.outputs.changed == 1 && steps.update-experiments-json.outputs.changed-branch == 1
with:
token: ${{ secrets.GITHUB_TOKEN }}

@ -1,11 +1,8 @@
name: Release Automation
name: Release
on:
push:
branches:
- fork
tags:
- '*'
create:
jobs:
release-automation:
name: Build App
@ -14,18 +11,34 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3
with:
submodules: 'true'
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v3
with:
java-version: 11
java-version: 17
distribution: temurin
- name: Install Android SDK with pieces Gradle skips
run: ./automation/iceraven/install-sdk.sh
- name: Inspect memory
run: free -m
run: free -h
- name: Create version name
run: echo "VERSION_NAME=$(git describe --tags HEAD)" >> $GITHUB_ENV
- name: Patch on the fly
run: |
sed -i 's#\.\./version.txt#\./version.txt#g' android-components/plugins/config/src/main/java/ConfigPlugin.kt
./automation/iceraven/patch_android_components.sh
- name: Relpace strings
run: |
sed -i 's/Firefox/Iceraven/g' app/src/*/res/values*/*strings.xml
sed -i '/about_content/s/Mozilla/@forkmaintainers/' app/src/*/res/values*/*strings.xml
- name: Build forkRelease variant of app
uses: gradle/gradle-build-action@v2
env:
@ -33,45 +46,51 @@ jobs:
# See <https://stackoverflow.com/a/70010526> and <https://stackoverflow.com/a/70756876>
# The runner seems to have ~6 gigs of memory, so we make sure to stay under that.
# We have Java 11 so we don't have a perm size anymore.
GRADLE_OPTS: -Dorg.gradle.jvmargs="-XX:MaxMetaspaceSize=2g -Xms2g -Xmx5g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dev/stderr"
GRADLE_OPTS: -Dorg.gradle.jvmargs="-XX:MaxMetaspaceSize=2g -Xms1g -Xmx3g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dev/stderr"
with:
gradle-home-cache-cleanup: true
gradle-executable: /usr/bin/time
arguments: -v ./gradlew app:assembleForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
arguments: -v ./gradlew app:assemblefenixForkRelease -x lintVitalFenixForkRelease -PversionName=${{ env.VERSION_NAME }} --stacktrace
- name: Setup build tool version variable
shell: bash
run: |
BUILD_TOOL_VERSION=$(ls /usr/local/lib/android/sdk/build-tools/ | tail -n 1)
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
- name: Create signed APKs
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
uses: abhijitvalluri/sign-apks@v0.8
with:
releaseDirectory: app/build/outputs/apk/forkRelease/
releaseDirectory: app/build/outputs/apk/fenix/forkRelease/
signingKeyBase64: ${{ secrets.DEBUG_SIGNING_KEY }}
alias: ${{ secrets.DEBUG_ALIAS }}
keyStorePassword: ${{ secrets.DEBUG_KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.DEBUG_KEY_PASSWORD }}
env:
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
- name: Create changelog
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
run: |
PREVIOUS_RELEASE_TAG=$(git tag --list iceraven-* --sort=-creatordate | tail -n+2 | head -n 1)
CURRENT_RELEASE_TAG=${{ github.event.ref }}
CURRENT_RELEASE_TAG_CAPITALIZE=${CURRENT_RELEASE_TAG^}
CURRENT_RELEASE_TAG_CAPITALIZE=$(echo $CURRENT_RELEASE_TAG_CAPITALIZE | tr '-' ' ')
echo "CURRENT_RELEASE_TAG_CAPITALIZE=${CURRENT_RELEASE_TAG_CAPITALIZE}" >> $GITHUB_ENV
FENIX_TAG=$(cat version.txt | tr -d '\n')
echo -e "## Release info\n" >> temp_changelog.md
echo "## Automated release of version ${{ github.event.ref }} browser" >>temp_changelog.md
if [[ "$(git log | grep $PREVIOUS_RELEASE_TAG | wc -l)" != "0" ]] ; then
# There's a path from the previous release to now
echo "<details>" >>temp_changelog.md
echo "<summary>Click to expand</summary>" >>temp_changelog.md
echo " " >>temp_changelog.md
echo "This is an automated release, consisting of the following changes:" >>temp_changelog.md
echo "### Change log (commit history since previous release)" >>temp_changelog.md
echo " " >>temp_changelog.md
git log ${{ github.event.ref }}...$PREVIOUS_RELEASE_TAG --pretty='format:%C(auto)%h (%as) %s' | head -n 1000 >>temp_changelog.md
echo " " >>temp_changelog.md
echo " " >>temp_changelog.md
echo "</details>" >>temp_changelog.md
else
echo "This is an automated release, not directly descended from the previous release." >>temp_changelog.md
fi
echo "**NOTE**: @fork-maintainers, you can edit these auto-generated release notes with a more user-friendly summary of the key changes, if needed." >>temp_changelog.md
echo " " >>temp_changelog.md
echo '```' >> temp_changelog.md
echo "Iceraven: $(echo $CURRENT_RELEASE_TAG | tr -d 'iceraven-')" >> temp_changelog.md
echo "Fenix: ${FENIX_TAG}" >> temp_changelog.md
echo -e '```\n' >> temp_changelog.md
echo -e "## News\n" >> temp_changelog.md
echo -e "## Change log\n" >> temp_changelog.md
echo "[${PREVIOUS_RELEASE_TAG}...${CURRENT_RELEASE_TAG}](https://github.com/${{ github.repository }}/compare/${PREVIOUS_RELEASE_TAG}...${CURRENT_RELEASE_TAG})" >> temp_changelog.md
- name: Create Release
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
@ -81,7 +100,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.event.ref }}
release_name: "Version ${{ github.event.ref }}"
release_name: "${{ env.CURRENT_RELEASE_TAG_CAPITALIZE }}"
draft: false
prerelease: false
body_path: temp_changelog.md
@ -93,11 +112,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/forkRelease/app-arm64-v8a-forkRelease.apk
asset_path: app/build/outputs/apk/fenix/forkRelease/app-fenix-arm64-v8a-forkRelease.apk
asset_name: ${{ github.event.ref }}-browser-arm64-v8a-forkRelease.apk
asset_content_type: application/vnd.android.package-archive
- name: Upload armeabi apk
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
uses: actions/upload-release-asset@v1
@ -105,11 +123,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/forkRelease/app-armeabi-v7a-forkRelease.apk
asset_path: app/build/outputs/apk/fenix/forkRelease/app-fenix-armeabi-v7a-forkRelease.apk
asset_name: ${{ github.event.ref }}-browser-armeabi-v7a-forkRelease.apk
asset_content_type: application/vnd.android.package-archive
- name: Upload x86 apk
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
uses: actions/upload-release-asset@v1
@ -117,11 +134,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/forkRelease/app-x86-forkRelease.apk
asset_path: app/build/outputs/apk/fenix/forkRelease/app-fenix-x86-forkRelease.apk
asset_name: ${{ github.event.ref }}-browser-x86-forkRelease.apk
asset_content_type: application/vnd.android.package-archive
- name: Upload x86_64 apk
if: "contains(toJSON(github.event.ref_type), 'tag') && contains(toJSON(github.event.ref), 'iceraven')"
uses: actions/upload-release-asset@v1
@ -129,6 +145,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/forkRelease/app-x86_64-forkRelease.apk
asset_path: app/build/outputs/apk/fenix/forkRelease/app-fenix-x86_64-forkRelease.apk
asset_name: ${{ github.event.ref }}-browser-x86_64-forkRelease.apk
asset_content_type: application/vnd.android.package-archive

@ -1,16 +0,0 @@
name: AssignTriageLabel
on:
issues:
types: [opened]
jobs:
assign:
name: Triage Issues
runs-on: ubuntu-latest
steps:
- name: Add Triage Label
uses: boek/AddTriageLabel@v1.2
with:
repotoken: ${{ secrets.GITHUB_TOKEN }}
labeltoadd: "needs:triage"

3
.gitignore vendored

@ -7,6 +7,7 @@
*.apk
*.ap_
*.aab
output-metadata.json
# Files for the ART/Dalvik VM
*.dex
@ -93,7 +94,7 @@ __pycache__/
venv/
# UI test artifacts
# UI test artifacts
.firebase_token*
results/
test_artifacts/

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "android-components"]
path = android-components
url = https://github.com/akliuxingyuan/android-components

@ -1,122 +0,0 @@
queue_rules:
- name: default
conditions:
- status-success=complete-pr
pull_request_rules:
- name: Resolve conflict
conditions:
- conflict
actions:
comment:
message: This pull request has conflicts when rebasing. Could you fix it @{{author}}? 🙏
- name: MickeyMoz - Auto Merge
conditions:
- author=MickeyMoz
- status-success=pr-complete
- files~=(Gecko.kt|AndroidComponents.kt)
actions:
review:
type: APPROVE
message: MickeyMoz 💪
queue:
method: rebase
name: default
rebase_fallback: none
- name: L10N - Auto Merge
conditions:
- author=mozilla-l10n-automation-bot
- status-success=pr-complete
- files~=(strings.xml|l10n.toml)
actions:
review:
type: APPROVE
message: LGTM 😎
queue:
method: rebase
name: default
rebase_fallback: none
- name: Release automation (Old)
conditions:
- base~=releases[_/].*
- author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-debug
- check-success=build-debug
- check-success=build-nightly-simulation
- check-success=lint-compare-locales
- check-success=lint-detekt
- check-success=lint-ktlint
- check-success=lint-lint
- check-success=signing-android-test-debug
- check-success=signing-debug
- check-success=signing-nightly-simulation
- check-success=test-debug
# TODO Temporarily disabled - should be renamed to -build
#- check-success=ui-test-x86-debug
- files~=(AndroidComponents.kt)
actions:
review:
type: APPROVE
message: 🚢
queue:
method: rebase
name: default
rebase_fallback: none
delete_head_branch:
force: false
- name: Release automation (New)
conditions:
- base~=releases[_/].*
- author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-beta
- check-success=build-android-test-debug
- check-success=build-beta-firebase
- check-success=build-debug
- check-success=build-nightly-simulation
- check-success=lint-compare-locales
- check-success=lint-detekt
- check-success=lint-ktlint
- check-success=lint-lint
- check-success=signing-android-test-beta
- check-success=signing-beta-firebase
- check-success=signing-nightly-simulation
- check-success=test-debug
- check-success=ui-test-x86-beta
- files~=(AndroidComponents.kt)
actions:
review:
type: APPROVE
message: 🚢
queue:
method: rebase
name: default
rebase_fallback: none
delete_head_branch:
force: false
- name: Needs landing - Rebase
conditions:
- check-success=pr-complete
- label=pr:needs-landing
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
queue:
method: rebase
name: default
rebase_fallback: none
- name: Needs landing - Squash
conditions:
- check-success=pr-complete
- label=pr:needs-landing-squashed
- "#approved-reviews-by>=1"
- -draft
- label!=pr:work-in-progress
- label!=pr:do-not-land
actions:
queue:
method: squash
name: default
rebase_fallback: none

@ -1,293 +0,0 @@
---
version: 1
reporting: checks-v1
policy:
# XXX We restrict taskcluster to collaborators so priviledged tests (like UI tests) can run on PRs
pullRequests: collaborators
tasks:
- $let:
trustDomain: mobile
# Github events have this stuff in different places...
ownerEmail:
$if: 'tasks_for in ["cron", "action"]'
then: '${tasks_for}@noreply.mozilla.org'
else:
$if: 'tasks_for == "github-push"'
then:
$if: 'event.pusher.email'
then: '${event.pusher.email}'
else: '${event.pusher.name}@users.noreply.github.com'
else:
$if: 'tasks_for == "github-pull-request"'
then: '${event.pull_request.user.login}@users.noreply.github.com'
baseRepoUrl:
$if: 'tasks_for == "github-push"'
then: '${event.repository.html_url}'
else:
$if: 'tasks_for == "github-pull-request"'
then: '${event.pull_request.base.repo.html_url}'
else:
$if: 'tasks_for in ["cron", "action"]'
then: '${repository.url}'
repoUrl:
$if: 'tasks_for == "github-push"'
then: '${event.repository.html_url}'
else:
$if: 'tasks_for == "github-pull-request"'
then: '${event.pull_request.head.repo.html_url}'
else:
$if: 'tasks_for in ["cron", "action"]'
then: '${repository.url}'
project:
$if: 'tasks_for == "github-push"'
then: '${event.repository.name}'
else:
$if: 'tasks_for == "github-pull-request"'
then: '${event.pull_request.head.repo.name}'
else:
$if: 'tasks_for in ["cron", "action"]'
then: '${repository.project}'
head_branch:
$if: 'tasks_for == "github-pull-request"'
then: ${event.pull_request.head.ref}
else:
$if: 'tasks_for == "github-push"'
then: ${event.ref}
else:
$if: 'tasks_for in ["action", "cron"]'
then: '${push.branch}'
head_sha:
$if: 'tasks_for == "github-push"'
then: '${event.after}'
else:
$if: 'tasks_for == "github-pull-request"'
then: '${event.pull_request.head.sha}'
else:
$if: 'tasks_for in ["action", "cron"]'
then: '${push.revision}'
ownTaskId:
$if: '"github" in tasks_for'
then: {$eval: as_slugid("decision_task")}
else:
$if: 'tasks_for in ["cron", "action"]'
then: '${ownTaskId}'
pullRequestAction:
$if: 'tasks_for == "github-pull-request"'
then: ${event.action}
else: 'UNDEFINED'
in:
$if: >
tasks_for in ["action", "cron"]
|| (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"])
|| (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") && (head_branch[:8] != "mergify/")
then:
$let:
level:
$if: 'tasks_for in ["github-push", "action", "cron"] && repoUrl == "https://github.com/mozilla-mobile/fenix"'
then: '3'
else: '1'
short_head_branch:
$if: 'head_branch[:11] == "refs/heads/"'
then: {$eval: 'head_branch[11:]'}
in:
taskId:
$if: 'tasks_for != "action"'
then: '${ownTaskId}'
taskGroupId:
$if: 'tasks_for == "action"'
then: '${action.taskGroupId}'
else: '${ownTaskId}' # same as taskId; this is how automation identifies a decision task
schedulerId: '${trustDomain}-level-${level}'
created: {$fromNow: ''}
deadline: {$fromNow: '1 day'}
expires: {$fromNow: '1 year 1 second'} # 1 second so artifacts expire first, despite rounding errors
metadata:
$merge:
- owner: "${ownerEmail}"
source: '${repoUrl}/raw/${head_sha}/.taskcluster.yml'
- $if: 'tasks_for in ["github-push", "github-pull-request"]'
then:
name: "Decision Task"
description: 'The task that creates all of the other tasks in the task graph'
else:
$if: 'tasks_for == "action"'
then:
name: "Action: ${action.title}"
description: |
${action.description}
Action triggered by clientID `${clientId}`
else:
name: "Decision Task for cron job ${cron.job_name}"
description: 'Created by a [cron task](https://firefox-ci-tc.services.mozilla.com/tasks/${cron.task_id})'
provisionerId: "${trustDomain}-${level}"
workerType: "decision-gcp"
tags:
$if: 'tasks_for in ["github-push", "github-pull-request"]'
then:
kind: decision-task
else:
$if: 'tasks_for == "action"'
then:
kind: 'action-callback'
else:
$if: 'tasks_for == "cron"'
then:
kind: cron-task
routes:
$flattenDeep:
- checks
- $if: 'level == "3" || repoUrl == "https://github.com/mozilla-releng/staging-fenix"'
then:
- tc-treeherder.v2.${project}.${head_sha}
# TODO Bug 1601928: Make this scope fork-friendly once ${project} is better defined. This will enable
# staging release promotion on forks.
- $if: 'tasks_for == "github-push"'
then:
- index.${trustDomain}.v2.${project}.branch.${short_head_branch}.latest.taskgraph.decision
- index.${trustDomain}.v2.${project}.branch.${short_head_branch}.revision.${head_sha}.taskgraph.decision
- index.${trustDomain}.v2.${project}.revision.${head_sha}.taskgraph.decision
- $if: 'tasks_for == "cron"'
then:
# cron context provides ${head_branch} as a short one
- index.${trustDomain}.v2.${project}.branch.${head_branch}.latest.taskgraph.decision-${cron.job_name}
- index.${trustDomain}.v2.${project}.branch.${head_branch}.revision.${head_sha}.taskgraph.decision-${cron.job_name}
- index.${trustDomain}.v2.${project}.branch.${head_branch}.revision.${head_sha}.taskgraph.cron.${ownTaskId}
scopes:
$if: 'tasks_for == "github-push"'
then:
# `https://` is 8 characters so, ${repoUrl[8:]} is the repository without the protocol.
- 'assume:repo:${repoUrl[8:]}:branch:${short_head_branch}'
else:
$if: 'tasks_for == "github-pull-request"'
then:
- 'assume:repo:github.com/${event.pull_request.base.repo.full_name}:pull-request'
else:
$if: 'tasks_for == "action"'
then:
# when all actions are hooks, we can calculate this directly rather than using a variable
- '${action.repo_scope}'
else:
- 'assume:repo:${repoUrl[8:]}:cron:${cron.job_name}'
requires: all-completed
priority: lowest
retries: 5
payload:
env:
# run-task uses these to check out the source; the inputs
# to `mach taskgraph decision` are all on the command line.
$merge:
- MOBILE_BASE_REPOSITORY: '${baseRepoUrl}'
MOBILE_HEAD_REPOSITORY: '${repoUrl}'
MOBILE_HEAD_REF: '${head_branch}'
MOBILE_HEAD_REV: '${head_sha}'
MOBILE_REPOSITORY_TYPE: git
MOBILE_PIP_REQUIREMENTS: taskcluster/requirements.txt
MOZ_AUTOMATION: "1"
REPOSITORIES: {$json: {mobile: "Fenix"}}
HG_STORE_PATH: /builds/worker/checkouts/hg-store
ANDROID_SDK_ROOT: /builds/worker/android-sdk
- $if: 'tasks_for in ["github-pull-request"]'
then:
MOBILE_PULL_REQUEST_NUMBER: '${event.pull_request.number}'
- $if: 'tasks_for == "action"'
then:
ACTION_TASK_GROUP_ID: '${action.taskGroupId}' # taskGroupId of the target task
ACTION_TASK_ID: {$json: {$eval: 'taskId'}} # taskId of the target task (JSON-encoded)
ACTION_INPUT: {$json: {$eval: 'input'}}
ACTION_CALLBACK: '${action.cb_name}'
features:
taskclusterProxy: true
chainOfTrust: true
# 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-625975b642c148be4c6f1d8ee5cedf7399f5d0dd33d275ff69d5934e3082d4a9@sha256:bfb26700182486e1c6c52701baea6f386fa39e5e25417423c27845933605ad43
maxRunTime: 1800
command:
- /usr/local/bin/run-task
- '--mobile-checkout=/builds/worker/checkouts/src'
- '--task-cwd=/builds/worker/checkouts/src'
- '--'
- bash
- -cx
- $let:
extraArgs:
$if: 'tasks_for == "cron"'
then: '${cron.quoted_args}'
else: ''
in:
$if: 'tasks_for == "action"'
then: >
taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph action-callback
else: >
taskcluster/scripts/decision-install-sdk.sh &&
ln -s /builds/worker/artifacts artifacts &&
~/.local/bin/taskgraph decision
--pushlog-id='0'
--pushdate='0'
--project='${project}'
--message=""
--owner='${ownerEmail}'
--level='${level}'
--base-repository="$MOBILE_BASE_REPOSITORY"
--head-repository="$MOBILE_HEAD_REPOSITORY"
--head-ref="$MOBILE_HEAD_REF"
--head-rev="$MOBILE_HEAD_REV"
--repository-type="$MOBILE_REPOSITORY_TYPE"
--tasks-for='${tasks_for}'
${extraArgs}
artifacts:
'public':
type: 'directory'
path: '/builds/worker/artifacts'
expires:
$fromNow: '1 year'
'public/docker-contexts':
type: 'directory'
path: '/builds/worker/checkouts/src/docker-contexts'
# This needs to be at least the deadline of the
# decision task + the docker-image task deadlines.
# It is set to a week to allow for some time for
# debugging, but they are not useful long-term.
expires:
$fromNow: '7 day'
extra:
$merge:
- treeherder:
$merge:
- machine:
platform: gecko-decision
- $if: 'tasks_for in ["github-push", "github-pull-request"]'
then:
symbol: D
else:
$if: 'tasks_for == "action"'
then:
groupName: 'action-callback'
groupSymbol: AC
symbol: "${action.symbol}"
else:
groupSymbol: cron
symbol: "${cron.job_symbol}"
- $if: 'tasks_for == "action"'
then:
parent: '${action.taskGroupId}'
action:
name: '${action.name}'
context:
taskGroupId: '${action.taskGroupId}'
taskId: {$eval: 'taskId'}
input: {$eval: 'input'}
clientId: {$eval: 'clientId'}
- $if: 'tasks_for == "cron"'
then:
cron: {$json: {$eval: 'cron'}}
- tasks_for: '${tasks_for}'

@ -1,16 +0,0 @@
language: android
dist: trusty
script:
# Prepare SDK
- sudo mkdir -p /usr/local/android-sdk/licenses/
- sudo touch /usr/local/android-sdk/licenses/android-sdk-license
- echo "8933bad161af4178b1185d1a37fbf41ea5269c55" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "d56f5187479451eabf01fb78af6dfcb131a6481e" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
- echo "24333f8a63b6825ea9c5514f83c2829b004d1fee" | sudo tee -a /usr/local/android-sdk/licenses/android-sdk-license
# The build needs this but Gradle refuses to fetch it.
- sdkmanager "ndk;21.0.6113669"
# Run tests
- ./gradlew -q testDebug 2>&1
# Make sure a release build builds
- ./gradlew assembleForkRelease -PversionName="$(git describe --tags HEAD)"

@ -1,8 +1,8 @@
# Community Participation Guidelines
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
This repository is governed by Mozilla's code of conduct and etiquette guidelines.
For more details, please read the
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/).
## How to Report
For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page.

@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

@ -1,4 +1,4 @@
# Iceraven Browser! [![Build Status](https://travis-ci.org/fork-maintainers/iceraven-browser.svg?branch=fork)](https://travis-ci.org/fork-maintainers/iceraven-browser) [![Release Automation](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/iceraven-build.yml/badge.svg)](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/iceraven-build.yml)
# Iceraven Browser! [![CI](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml/badge.svg)](https://github.com/fork-maintainers/iceraven-browser/actions/workflows/ci.yml) ![Release](https://img.shields.io/github/v/release/fork-maintainers/iceraven-browser)
Definitely not brought to you by Mozilla!
@ -9,6 +9,8 @@ Our goal is to be a close fork of the new Firefox for Android that seeks to prov
Notable features include:
* `about:config` support
* The ability to *attempt* to install a much longer list of add-ons than Mozilla's Fenix version of Firefox accepts. Currently the browser queries [this AMO collection](https://addons.mozilla.org/en-US/firefox/collections/16201230/What-I-want-on-Fenix/) **Most of them will not work**, because they depend on code that Mozilla is still working on writing in `android-components`, but you may attempt to install them. If you don't see an add-on you want, you can [request it](https://github.com/fork-maintainers/iceraven-browser/issues/new).
* Option to suspend tabs to avoid being killed for memory (https://bugzilla.mozilla.org/show_bug.cgi?id=1807364)
* Option not to display recently visited websites at HomePage
* **No warranties or guarantees of security or updates or even stability**! Note that Iceraven Browser includes some unstable code written by Mozilla, with our own added modifications on top, all shipped with the stable version of GeckoView engine. Hence, the browser may contain bugs introduced upstream. Binaries are currently built automatically by our Github release automation. These binaries are signed with a debug key. When we finally publish this somewhere official like F-droid, we will sign the apks with a proper key suitable for public release. Due to the current way we create the releases and sign them, you may not want to rely on such "alpha" quality software as your primary web browser, as it will have bugs. So, use this browser only if you are comfortable with these limitations/potential risks.
**Note/Disclaimer:** Iceraven Browser could not exist without the hardworking folks at the Mozilla Corporation who work on the Mozilla Android Components and Firefox projects, but it is not an official Mozilla product, and is not provided, endorsed, vetted, approved, or secured by Mozilla.
@ -61,7 +63,7 @@ cd ..
2. Clone the project.
```sh
git clone https://github.com/fork-maintainers/iceraven-browser
git clone --recursive https://github.com/fork-maintainers/iceraven-browser
```
4. Go inside `iceraven-browser`. That's where the build is coordinated from.
@ -73,18 +75,18 @@ cd iceraven-browser
5. Configure the project. For your personal use you need to sign the apk file. The simplest way to do this is to use the debug key that is auto-generated by Android SDK. This is not a great idea for releasing, but acceptable for your personal use. You can configure it as follows:
```sh
echo "autosignReleaseWithDebugKey=" >>local.properties
echo "autosignReleaseWithDebugKey=" >> local.properties
```
6. Build the project. To build the Iceraven-branded release APKs, you can do:
```sh
./gradlew app:assembleForkRelease -PversionName="$(git describe --tags HEAD)"
./gradlew app:assemblefenixForkRelease -PversionName="$(git describe --tags HEAD)"
```
(If you don't use the `app:` prefix, you might get complaints about the build system being `unable to locate the objcopy executable`.)
The APKs will show up in `app/build/outputs/apk/forkRelease/`.
The APKs will show up in `app/build/outputs/apk/fenix/forkRelease/`.
## Getting Involved

@ -0,0 +1 @@
Subproject commit c3b388f801ef6764f709ca1ccc6f5d770a944376

@ -0,0 +1,242 @@
---
cookie-banners:
description: Features for cookie banner handling.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
extensions-process:
description: A feature to rollout the extensions process.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the extensions process is enabled."
fx-suggest:
description: A feature that provides Firefox Suggest search suggestions.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "Whether the feature is enabled. When Firefox Suggest is enabled, Firefox will download and store new search suggestions in the background, and show additional Search settings to control which suggestions appear in the awesomebar. When Firefox Suggest is disabled, Firefox will not download new suggestions, and hide the additional Search settings.\n"
glean:
description: A feature that provides server-side configurations for Glean metrics (aka Server Knobs).
hasExposure: true
exposureDescription: ""
variables:
enable-event-timestamps:
type: boolean
description: Enables precise event timestamps for Glean events
metrics-enabled:
type: json
description: "A map of metric base-identifiers to booleans representing the state of the 'enabled' flag for that metric."
growth-data:
description: A feature measuring campaign growth data
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active"
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: "This property provides a lookup table of whether or not the given section should be enabled. If the section is enabled, it should be toggleable in the settings screen, and on by default."
juno-onboarding:
description: A feature that shows juno onboarding flow.
hasExposure: true
exposureDescription: ""
variables:
cards:
type: json
description: Collection of user facing onboarding cards.
conditions:
type: json
description: "A collection of out the box conditional expressions to be used in determining whether a card should show or not. Each entry maps to a valid JEXL expression.\n"
messaging:
description: "The in-app messaging system.\n"
hasExposure: true
exposureDescription: ""
variables:
actions:
type: json
description: A growable map of action URLs.
message-under-experiment:
type: string
description: "Deprecated in favor of `MessageData#experiment`. This will be removed in future releases."
messages:
type: json
description: A growable collection of messages
notification-config:
type: json
description: Configuration of the notification worker for all notification messages.
on-control:
type: string
description: What should be displayed when a control message is selected.
enum:
- show-next-message
- show-none
styles:
type: json
description: "A map of styles to configure message appearance.\n"
triggers:
type: json
description: "A collection of out the box trigger expressions. Each entry maps to a valid JEXL expression.\n"
mr2022:
description: Features for MR 2022.
hasExposure: true
exposureDescription: ""
variables:
sections-enabled:
type: json
description: This property provides a lookup table of whether or not the given section should be enabled.
nimbus-system:
description: "Configuration of the Nimbus System in Android.\n"
hasExposure: true
exposureDescription: ""
variables:
refresh-interval-foreground:
type: int
description: "The minimum interval in minutes between fetching experiment \nrecipes in the foreground.\n"
nimbus-validation:
description: A feature that does not correspond to an application feature suitable for showing that Nimbus is working. This should never be used in production.
hasExposure: true
exposureDescription: ""
variables:
settings-icon:
type: string
description: The drawable displayed in the app menu for Settings
settings-punctuation:
type: string
description: The emoji displayed in the Settings screen title.
settings-title:
type: string
description: The title of displayed in the Settings screen and app menu.
onboarding:
description: "A feature that configures the new user onboarding page. Note that onboarding is a **first run** feature, and should only be modified by first run experiments."
hasExposure: true
exposureDescription: ""
variables:
order:
type: json
description: Determines the order of the onboarding page panels
pdfjs:
description: PDF.js features
hasExposure: true
exposureDescription: ""
variables:
download-button:
type: boolean
description: Download button
open-in-app-button:
type: boolean
description: Open in app button
pre-permission-notification-prompt:
description: A feature that shows the pre-permission notification prompt.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, the pre-permission notification prompt is shown to the user."
print:
description: A feature for printing from the share or browser menu.
hasExposure: true
exposureDescription: ""
variables:
browser-print-enabled:
type: boolean
description: "If true, a print button from the browser menu is available."
share-print-enabled:
type: boolean
description: "If true, a print button from the share menu is available."
private-browsing:
description: Private Browsing Mode
hasExposure: true
exposureDescription: ""
variables:
felt-privacy-enabled:
type: boolean
description: "if true, enable felt privacy related UI"
re-engagement-notification:
description: A feature that shows the re-engagement notification if the user is inactive.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the re-engagement notification is shown to the inactive user."
type:
type: int
description: The type of re-engagement notification that is shown to the inactive user.
search-extra-params:
description: A feature that provides additional args for search.
hasExposure: true
exposureDescription: ""
variables:
channel-id:
type: json
description: The channel Id param name with arg.
enabled:
type: boolean
description: "If true, the feature is active."
feature-enabler:
type: json
description: "The feature enabler param name with arg, NOTE this map could be empty."
search-engine:
type: string
description: The search engine name.
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature shows up on the homescreen and on the new tab screen."
shopping-experience:
description: A feature that shows product review quality information.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "if true, the shopping experience feature is shown to the user."
product-recommendations:
type: boolean
description: "if true, recommended products feature is enabled to be shown to the user based on their preference."
splash-screen:
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature is active."
maximum_duration_ms:
type: int
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
toolbar:
description: The searchbar/awesomebar that user uses to search.
hasExposure: true
exposureDescription: ""
variables:
toolbar-position-top:
type: boolean
description: "If true, toolbar appears at top of the screen."
unified-search:
description: A feature allowing user to easily search for specified results directly in the search bar.
hasExposure: true
exposureDescription: ""
variables:
enabled:
type: boolean
description: "If true, the feature shows up in the search bar."

@ -1,8 +1,9 @@
import org.mozilla.fenix.gradle.tasks.ApkSizeTask
import com.android.build.api.variant.FilterConfiguration
import org.apache.tools.ant.util.StringUtils
plugins {
id "com.jetbrains.python.envs" version "0.0.26"
id "com.google.protobuf" version "0.8.19"
id "com.jetbrains.python.envs" version "$python_envs_plugin"
id "com.google.protobuf" version "$protobuf_plugin"
}
apply plugin: 'com.android.application'
@ -11,21 +12,17 @@ apply plugin: 'kotlin-parcelize'
apply plugin: 'jacoco'
apply plugin: 'androidx.navigation.safeargs.kotlin'
apply plugin: 'com.google.android.gms.oss-licenses-plugin'
apply plugin: 'androidx.benchmark'
import com.android.build.OutputFile
import groovy.json.JsonOutput
import org.gradle.internal.logging.text.StyledTextOutput.Style
import org.gradle.internal.logging.text.StyledTextOutputFactory
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import static org.gradle.api.tasks.testing.TestResult.ResultType
apply from: 'benchmark.gradle'
android {
compileSdkVersion Config.compileSdkVersion
project.maybeConfigForJetpackBenchmark(it)
if (project.hasProperty("testBuildType")) {
// Allowing to configure the test build type via command line flag (./gradlew -PtestBuildType=beta ..)
@ -35,8 +32,9 @@ android {
defaultConfig {
applicationId "io.github.forkmaintainers"
minSdkVersion Config.minSdkVersion
targetSdkVersion Config.targetSdkVersion
minSdkVersion config.minSdkVersion
compileSdk config.compileSdkVersion
targetSdkVersion config.targetSdkVersion
versionCode 1
versionName Config.generateDebugVersionName()
vectorDrawables.useSupportLibrary = true
@ -64,19 +62,28 @@ android {
"}"
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
]
// This allows overriding the target activity for MozillaOnline builds, which happens
// as part of the defaultConfig below.
def targetActivity = "HomeActivity"
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
if (project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")) {
buildConfigField "boolean", "MOZILLA_ONLINE", "true"
targetActivity = "MozillaOnlineHomeActivity"
} else {
buildConfigField "boolean", "MOZILLA_ONLINE", "false"
}
manifestPlaceholders = [
"targetActivity": targetActivity,
"deepLinkScheme": deepLinkSchemeValue
]
buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", getSupportedLocales()
}
def releaseTemplate = {
@ -107,90 +114,96 @@ android {
applicationIdSuffix ".fenix.debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": false
]
}
nightly releaseTemplate >> {
applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue, "requestLegacyExternalStorage": false]
manifestPlaceholders.putAll([
"deepLinkScheme": deepLinkSchemeValue
])
}
beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".firefox_beta"
def deepLinkSchemeValue = "fenix-beta"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
manifestPlaceholders.putAll([
// This release type is meant to replace Firefox (Beta channel) and therefore needs to inherit
// its sharedUserId for all eternity. See:
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false&regexp=false&path=
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false&regexp=false&path=
// Shipping an app update without sharedUserId can have
// fatal consequences. For example see:
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
]
])
}
release releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".firefox"
def deepLinkSchemeValue = "fenix"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
manifestPlaceholders.putAll([
// This release type is meant to replace Firefox (Release channel) and therefore needs to inherit
// its sharedUserId for all eternity. See:
// https://searchfox.org/mozilla-central/search?q=moz_android_shared_id&case=false&regexp=false&path=
// https://searchfox.org/mozilla-esr68/search?q=moz_android_shared_id&case=false&regexp=false&path=
// Shipping an app update without sharedUserId can have
// fatal consequences. For example see:
// - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
]
])
}
forkDebug {
shrinkResources false
minifyEnabled false
applicationIdSuffix ".iceraven.debug"
resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true
// Need to replicate default debug config features
signingConfig signingConfigs.debug
debuggable true
def deepLinkSchemeValue = "iceraven-debug"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders.putAll([
"sharedUserId": "io.github.forkmaintainers.iceraven.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
])
// Use custom default allowed addon list
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"18187371\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"Iceraven_Addon_Collection\""
resValue "bool", "IS_DEBUG", "true"
matchingFallbacks = ['debug']
}
forkRelease releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
applicationIdSuffix ".iceraven"
def deepLinkSchemeValue = "iceraven"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue
]
manifestPlaceholders.putAll([
"sharedUserId": "io.github.forkmaintainers.iceraven.sharedID",
"deepLinkScheme": deepLinkSchemeValue,
])
// Use custom default allowed addon list
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"18187371\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"Iceraven_Addon_Collection\""
}
benchmark releaseTemplate >> {
initWith buildTypes.nightly
applicationIdSuffix ".fenix"
debuggable false
}
}
buildFeatures {
viewBinding true
buildConfig true
}
aaptOptions {
androidResources {
// All JavaScript code used internally by GeckoView is packaged in a
// file called omni.ja. If this file is compressed in the APK,
// GeckoView must uncompress it before it can do anything else which
@ -208,7 +221,13 @@ android {
animationsDisabled = true
}
flavorDimensions "engine"
flavorDimensions.add("product")
productFlavors {
fenix {
dimension "product"
}
}
sourceSets {
androidTest {
@ -222,26 +241,54 @@ android {
reset()
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64"
// As gradle is unable to pick the right apk to install when multiple apks are generated
// while running benchmark tests or generating baseline profiles. To circumvent this,
// this flag is passed to make sure only one apk is generated so gradle can pick that one.
if (project.hasProperty("benchmarkTest")) {
include "arm64-v8a"
} else {
include "x86", "armeabi-v7a", "arm64-v8a", "x86_64"
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
lintOptions {
bundle {
// Profiler issues require us to temporarily package native code compressed to
// match the previous APK packaging.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1865634
packagingOptions {
jniLibs {
it.useLegacyPackaging = true
}
}
language {
// Because we have runtime language selection we will keep all strings and languages
// in the base APKs.
enableSplit = false
}
}
lint {
lintConfig file("lint.xml")
baseline file("lint-baseline.xml")
}
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
resources {
excludes += ['META-INF/atomicfu.kotlin_module', 'META-INF/AL2.0', 'META-INF/LGPL2.1',
'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
}
jniLibs {
useLegacyPackaging true
}
}
testOptions {
unitTests.returnDefaultValues = true
@ -261,12 +308,13 @@ android {
}
composeOptions {
kotlinCompilerExtensionVersion = FenixVersions.androidx_compose_compiler
kotlinCompilerExtensionVersion = Versions.compose_compiler
}
namespace 'org.mozilla.fenix'
}
android.applicationVariants.all { variant ->
android.applicationVariants.configureEach { variant ->
// -------------------------------------------------------------------------------------------------
// Generate version codes for builds
@ -277,12 +325,11 @@ android.applicationVariants.all { variant ->
println("----------------------------------------------")
println("Variant name: " + variant.name)
println("Application ID: " + [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Application ID: " + [variant.applicationId, variant.buildType.applicationIdSuffix].findAll().join())
println("Build type: " + variant.buildType.name)
println("Flavor: " + variant.flavorName)
println("Telemetry enabled: " + !isDebug)
if (useReleaseVersioning) {
// The Google Play Store does not allow multiple APKs for the same app that all have the
// same version code. Therefore we need to have different version codes for our ARM and x86
@ -293,16 +340,19 @@ android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def isMozillaOnline = project.hasProperty("mozillaOnline") || gradle.hasProperty("localProperties.mozillaOnline")
def abi = output.getFilter(OutputFile.ABI)
def abi = output.getFilter(FilterConfiguration.FilterType.ABI.name())
// If it is a Mozilla Online build, use a unified version code of armeabi-v7a
def arch = (isMozillaOnline) ? "armeabi-v7a" : abi
def aab = project.hasProperty("aab")
// We use the same version code generator, that we inherited from Fennec, across all channels - even on
// channels that never shipped a Fennec build.
def versionCodeOverride = Config.generateFennecVersionCode(arch)
def versionCodeOverride = Config.generateFennecVersionCode(arch, aab)
println("versionCode for $abi = $versionCodeOverride, isMozillaOnline = $isMozillaOnline")
output.versionNameOverride = versionName
if (versionName != null) {
output.versionNameOverride = versionName
}
output.versionCodeOverride = versionCodeOverride
}
} else if (gradle.hasProperty("localProperties.branchBuild.fenix.version")) {
@ -435,7 +485,8 @@ android.applicationVariants.all { variant ->
print("Wallpaper URL: ")
try {
def token = new File("${rootDir}/.wallpaper_url").text.trim()
// def token = new File("${rootDir}/.wallpaper_url").text.trim()
def token = "https://assets.mozilla.net/mobile-wallpapers/android"
buildConfigField 'String', 'WALLPAPER_URL', '"' + token + '"'
println "(Added from .wallpaper_url file)"
} catch (FileNotFoundException ignored) {
@ -457,195 +508,204 @@ android.applicationVariants.all { variant ->
buildConfigField 'String', 'POCKET_CONSUMER_KEY', '""'
println("--")
}
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag to disable LeakCanary in debug (on CI builds)
// -------------------------------------------------------------------------------------------------
if (isDebug) {
if (project.hasProperty("disableLeakCanary") || gradle.hasProperty("localProperties.disableLeakCanary")) {
buildConfigField "boolean", "LEAKCANARY", "false"
println("LeakCanary enabled in debug: false")
} else {
buildConfigField "boolean", "LEAKCANARY", "true"
println("LeakCanary enabled in debug: true")
}
} else {
buildConfigField "boolean", "LEAKCANARY", "false"
}
}
// Generate Kotlin code for the Fenix Glean metrics.
apply plugin: "org.mozilla.telemetry.glean-gradle-plugin"
apply plugin: "org.mozilla.components.nimbus-gradle-plugin"
apply plugin: "org.mozilla.appservices.nimbus-gradle-plugin"
nimbus {
// The path to the Nimbus feature manifest file
manifestFile = "nimbus.fml.yaml"
// The fully qualified class name for the generated features.
// If the classname begins with a '.' this is taken as a suffix to the app's package name
destinationClass = ".nimbus.FxNimbus"
// Map from the variant name to the channel as experimenter and nimbus understand it.
// If nimbus's channels were accurately set up well for this project, then this
// shouldn't be needed.
channels = [
debug: "developer",
nightly: "nightly",
beta: "beta",
release: "release",
fenixDebug: "developer",
fenixNightly: "nightly",
fenixBeta: "beta",
fenixRelease: "release",
fenixForkDebug: "forkDebug",
fenixForkRelease: "forkRelease",
fenixBenchmark: "developer"
]
// This is generated by the FML and should be checked into git.
// It will be fetched by Experimenter (the Nimbus experiment website)
// and used to inform experiment configuration.
experimenterManifest = ".experimenter.yaml"
}
configurations {
// There's an interaction between Gradle's resolution of dependencies with different types
// (@jar, @aar) for `implementation` and `testImplementation` and with Android Studio's built-in
// JUnit test runner. The runtime classpath in the built-in JUnit test runner gets the
// dependency from the `implementation`, which is type @aar, and therefore the JNA dependency
// doesn't provide the JNI dispatch libraries in the correct Java resource directories. I think
// what's happening is that @aar type in `implementation` resolves to the @jar type in
// `testImplementation`, and that it wins the dependency resolution battle.
//
// A workaround is to add a new configuration which depends on the @jar type and to reference
// the underlying JAR file directly in `testImplementation`. This JAR file doesn't resolve to
// the @aar type in `implementation`. This works when invoked via `gradle`, but also sets the
// correct runtime classpath when invoked with Android Studio's built-in JUnit test runner.
// Success!
jnaForTest
applicationServicesDir = gradle.hasProperty('localProperties.autoPublish.application-services.dir')
? gradle.getProperty('localProperties.autoPublish.application-services.dir') : null
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
tasks.withType(KotlinCompile).configureEach {
kotlinOptions.freeCompilerArgs += "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
dependencies {
jnaForTest FenixDependencies.jna
testImplementation files(configurations.jnaForTest.copyRecursive().files)
implementation FenixDependencies.mozilla_browser_engine_gecko
implementation FenixDependencies.kotlin_stdlib
implementation FenixDependencies.kotlin_coroutines
implementation FenixDependencies.kotlin_coroutines_android
testImplementation FenixDependencies.kotlin_coroutines_test
implementation FenixDependencies.androidx_appcompat
implementation FenixDependencies.androidx_constraintlayout
implementation FenixDependencies.androidx_coordinatorlayout
implementation project(':browser-engine-gecko')
implementation ComponentsDependencies.kotlin_coroutines
testImplementation ComponentsDependencies.testing_coroutines
implementation ComponentsDependencies.androidx_appcompat
implementation ComponentsDependencies.androidx_constraintlayout
implementation ComponentsDependencies.androidx_coordinatorlayout
implementation FenixDependencies.google_accompanist_drawablepainter
implementation FenixDependencies.google_accompanist_insets
implementation FenixDependencies.sentry
implementation FenixDependencies.mozilla_compose_awesomebar
implementation FenixDependencies.mozilla_concept_awesomebar
implementation FenixDependencies.mozilla_concept_base
implementation FenixDependencies.mozilla_concept_engine
implementation FenixDependencies.mozilla_concept_menu
implementation FenixDependencies.mozilla_concept_push
implementation FenixDependencies.mozilla_concept_storage
implementation FenixDependencies.mozilla_concept_sync
implementation FenixDependencies.mozilla_concept_toolbar
implementation FenixDependencies.mozilla_concept_tabstray
implementation FenixDependencies.mozilla_browser_domains
implementation FenixDependencies.mozilla_browser_icons
implementation FenixDependencies.mozilla_browser_menu
implementation FenixDependencies.mozilla_browser_menu2
implementation FenixDependencies.mozilla_browser_session_storage
implementation FenixDependencies.mozilla_browser_state
implementation FenixDependencies.mozilla_browser_storage_sync
implementation FenixDependencies.mozilla_browser_tabstray
implementation FenixDependencies.mozilla_browser_thumbnails
implementation FenixDependencies.mozilla_browser_toolbar
implementation FenixDependencies.mozilla_feature_addons
implementation FenixDependencies.mozilla_feature_accounts
implementation FenixDependencies.mozilla_feature_app_links
implementation FenixDependencies.mozilla_feature_autofill
implementation FenixDependencies.mozilla_feature_awesomebar
implementation FenixDependencies.mozilla_feature_contextmenu
implementation FenixDependencies.mozilla_feature_customtabs
implementation FenixDependencies.mozilla_feature_downloads
implementation FenixDependencies.mozilla_feature_intent
implementation FenixDependencies.mozilla_feature_media
implementation FenixDependencies.mozilla_feature_prompts
implementation FenixDependencies.mozilla_feature_push
implementation FenixDependencies.mozilla_feature_privatemode
implementation FenixDependencies.mozilla_feature_pwa
implementation FenixDependencies.mozilla_feature_qr
implementation FenixDependencies.mozilla_feature_search
implementation FenixDependencies.mozilla_feature_session
implementation FenixDependencies.mozilla_feature_syncedtabs
implementation FenixDependencies.mozilla_feature_toolbar
implementation FenixDependencies.mozilla_feature_tabs
implementation FenixDependencies.mozilla_feature_findinpage
implementation FenixDependencies.mozilla_feature_logins
implementation FenixDependencies.mozilla_feature_site_permissions
implementation FenixDependencies.mozilla_feature_readerview
implementation FenixDependencies.mozilla_feature_tab_collections
implementation FenixDependencies.mozilla_feature_recentlyclosed
implementation FenixDependencies.mozilla_feature_top_sites
implementation FenixDependencies.mozilla_feature_share
implementation FenixDependencies.mozilla_feature_accounts_push
implementation FenixDependencies.mozilla_feature_webauthn
implementation FenixDependencies.mozilla_feature_webcompat
implementation FenixDependencies.mozilla_feature_webnotifications
implementation FenixDependencies.mozilla_feature_webcompat_reporter
implementation FenixDependencies.mozilla_service_pocket
implementation FenixDependencies.mozilla_service_contile
implementation FenixDependencies.mozilla_service_digitalassetlinks
implementation FenixDependencies.mozilla_service_sync_autofill
implementation FenixDependencies.mozilla_service_sync_logins
implementation FenixDependencies.mozilla_service_firefox_accounts
implementation(FenixDependencies.mozilla_service_glean)
implementation FenixDependencies.mozilla_service_location
implementation FenixDependencies.mozilla_service_nimbus
implementation FenixDependencies.mozilla_support_extensions
implementation FenixDependencies.mozilla_support_base
implementation FenixDependencies.mozilla_support_rusterrors
implementation FenixDependencies.mozilla_support_images
implementation FenixDependencies.mozilla_support_ktx
implementation FenixDependencies.mozilla_support_rustlog
implementation FenixDependencies.mozilla_support_utils
implementation FenixDependencies.mozilla_support_locale
implementation FenixDependencies.mozilla_ui_colors
implementation FenixDependencies.mozilla_ui_icons
implementation FenixDependencies.mozilla_lib_publicsuffixlist
implementation FenixDependencies.mozilla_ui_widgets
implementation FenixDependencies.mozilla_ui_tabcounter
implementation FenixDependencies.mozilla_lib_crash
implementation FenixDependencies.lib_crash_sentry
implementation FenixDependencies.mozilla_lib_state
implementation FenixDependencies.mozilla_lib_dataprotect
debugImplementation FenixDependencies.leakcanary
implementation FenixDependencies.androidx_annotation
implementation FenixDependencies.androidx_compose_ui
implementation FenixDependencies.androidx_compose_ui_tooling
implementation FenixDependencies.androidx_compose_foundation
implementation FenixDependencies.androidx_compose_material
implementation ComponentsDependencies.thirdparty_sentry
implementation project(':compose-awesomebar')
implementation project(':compose-cfr')
implementation project(':concept-awesomebar')
implementation project(':concept-base')
implementation project(':concept-engine')
implementation project(':concept-menu')
implementation project(':concept-push')
implementation project(':concept-storage')
implementation project(':concept-sync')
implementation project(':concept-toolbar')
implementation project(':concept-tabstray')
implementation project(':browser-domains')
implementation project(':browser-icons')
implementation project(':browser-menu')
implementation project(':browser-menu2')
implementation project(':browser-session-storage')
implementation project(':browser-state')
implementation project(':browser-storage-sync')
implementation project(':browser-tabstray')
implementation project(':browser-thumbnails')
implementation project(':browser-toolbar')
implementation project(':feature-addons')
implementation project(':feature-accounts')
implementation project(':feature-app-links')
implementation project(':feature-autofill')
implementation project(':feature-awesomebar')
implementation project(':feature-contextmenu')
implementation project(':feature-customtabs')
implementation project(':feature-downloads')
implementation project(':feature-fxsuggest')
implementation project(':feature-intent')
implementation project(':feature-media')
implementation project(':feature-prompts')
implementation project(':feature-push')
implementation project(':feature-privatemode')
implementation project(':feature-pwa')
implementation project(':feature-qr')
implementation project(':feature-search')
implementation project(':feature-session')
implementation project(':feature-syncedtabs')
implementation project(':feature-toolbar')
implementation project(':feature-tabs')
implementation project(':feature-findinpage')
implementation project(':feature-logins')
implementation project(':feature-sitepermissions')
implementation project(':feature-readerview')
implementation project(':feature-tab-collections')
implementation project(':feature-recentlyclosed')
implementation project(':feature-top-sites')
implementation project(':feature-share')
implementation project(':feature-accounts-push')
implementation project(':feature-webauthn')
implementation project(':feature-webcompat')
implementation project(':feature-webnotifications')
implementation project(':feature-webcompat-reporter')
implementation project(':service-pocket')
implementation project(':service-contile')
implementation project(':service-digitalassetlinks')
implementation project(':service-sync-autofill')
implementation project(':service-sync-logins')
implementation project(':service-firefox-accounts')
implementation project(':service-glean')
implementation project(':service-location')
implementation project(':service-nimbus')
implementation project(':support-webextensions')
implementation project(':support-base')
implementation project(':support-rusterrors')
implementation project(':support-images')
implementation project(':support-ktx')
implementation project(':support-rustlog')
implementation project(':support-utils')
implementation project(':support-locale')
implementation project(':ui-colors')
implementation project(':ui-icons')
implementation project(':lib-publicsuffixlist')
implementation project(':ui-widgets')
implementation project(':ui-tabcounter')
implementation project(':lib-crash')
implementation project(':lib-crash-sentry')
implementation project(':lib-state')
implementation project(':lib-dataprotect')
debugImplementation ComponentsDependencies.leakcanary
forkDebugImplementation ComponentsDependencies.leakcanary
debugImplementation ComponentsDependencies.androidx_compose_ui_tooling
implementation ComponentsDependencies.androidx_activity_compose
implementation FenixDependencies.androidx_activity_ktx
implementation ComponentsDependencies.androidx_annotation
implementation ComponentsDependencies.androidx_compose_ui
implementation ComponentsDependencies.androidx_compose_ui_tooling_preview
implementation ComponentsDependencies.androidx_compose_animation
implementation ComponentsDependencies.androidx_compose_foundation
implementation ComponentsDependencies.androidx_compose_material
implementation FenixDependencies.androidx_legacy
implementation FenixDependencies.androidx_biometric
implementation FenixDependencies.androidx_paging
implementation FenixDependencies.androidx_preference
implementation FenixDependencies.androidx_fragment
implementation FenixDependencies.androidx_navigation_fragment
implementation FenixDependencies.androidx_navigation_ui
implementation FenixDependencies.androidx_recyclerview
implementation FenixDependencies.androidx_lifecycle_common
implementation FenixDependencies.androidx_lifecycle_livedata
implementation FenixDependencies.androidx_lifecycle_process
implementation FenixDependencies.androidx_lifecycle_runtime
implementation FenixDependencies.androidx_lifecycle_viewmodel
implementation FenixDependencies.androidx_core
implementation FenixDependencies.androidx_core_ktx
implementation ComponentsDependencies.androidx_biometric
implementation ComponentsDependencies.androidx_paging
implementation ComponentsDependencies.androidx_preferences
implementation ComponentsDependencies.androidx_fragment
implementation ComponentsDependencies.androidx_navigation_fragment
implementation ComponentsDependencies.androidx_navigation_ui
implementation ComponentsDependencies.androidx_compose_navigation
implementation ComponentsDependencies.androidx_recyclerview
implementation ComponentsDependencies.androidx_lifecycle_common
implementation ComponentsDependencies.androidx_lifecycle_livedata
implementation ComponentsDependencies.androidx_lifecycle_process
implementation ComponentsDependencies.androidx_lifecycle_runtime
implementation ComponentsDependencies.androidx_lifecycle_viewmodel
implementation ComponentsDependencies.androidx_lifecycle_service
implementation ComponentsDependencies.androidx_core
implementation ComponentsDependencies.androidx_core_ktx
implementation FenixDependencies.androidx_core_splashscreen
implementation FenixDependencies.androidx_transition
implementation FenixDependencies.androidx_work_ktx
implementation ComponentsDependencies.androidx_work_runtime
implementation FenixDependencies.androidx_datastore
implementation ComponentsDependencies.androidx_data_store_preferences
implementation FenixDependencies.protobuf_javalite
implementation FenixDependencies.google_material
implementation ComponentsDependencies.google_material
androidTestImplementation FenixDependencies.uiautomator
androidTestImplementation "tools.fastlane:screengrab:2.0.0"
androidTestImplementation ComponentsDependencies.androidx_test_uiautomator
androidTestImplementation FenixDependencies.fastlane
// This Falcon version is added to maven central now required for Screengrab
implementation 'com.jraska:falcon:2.2.0'
androidTestImplementation FenixDependencies.falcon
androidTestImplementation FenixDependencies.androidx_compose_ui_test
androidTestImplementation ComponentsDependencies.androidx_compose_ui_test
androidTestImplementation FenixDependencies.espresso_core, {
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
@ -659,33 +719,31 @@ dependencies {
exclude module: 'protobuf-lite'
}
androidTestImplementation FenixDependencies.androidx_test_core
androidTestImplementation ComponentsDependencies.androidx_test_core
androidTestImplementation FenixDependencies.espresso_idling_resources
androidTestImplementation FenixDependencies.espresso_intents
androidTestImplementation FenixDependencies.tools_test_runner
androidTestImplementation FenixDependencies.tools_test_rules
androidTestImplementation ComponentsDependencies.androidx_test_runner
androidTestImplementation ComponentsDependencies.androidx_test_rules
androidTestUtil FenixDependencies.orchestrator
androidTestImplementation FenixDependencies.espresso_core, {
androidTestImplementation ComponentsDependencies.androidx_espresso_core, {
exclude group: 'com.android.support', module: 'support-annotations'
}
androidTestImplementation FenixDependencies.androidx_junit
androidTestImplementation FenixDependencies.androidx_test_extensions
androidTestImplementation FenixDependencies.androidx_work_testing
androidTestImplementation ComponentsDependencies.androidx_test_junit
androidTestImplementation ComponentsDependencies.androidx_work_testing
androidTestImplementation FenixDependencies.androidx_benchmark_junit4
androidTestImplementation FenixDependencies.mockwebserver
testImplementation FenixDependencies.mozilla_support_test
testImplementation FenixDependencies.mozilla_support_test_libstate
testImplementation FenixDependencies.androidx_junit
testImplementation FenixDependencies.androidx_test_extensions
testImplementation FenixDependencies.androidx_work_testing
testImplementation (FenixDependencies.robolectric) {
androidTestImplementation ComponentsDependencies.testing_mockwebserver
testImplementation project(':support-test')
testImplementation project(':support-test-libstate')
testImplementation ComponentsDependencies.androidx_test_junit
testImplementation ComponentsDependencies.androidx_work_testing
testImplementation (ComponentsDependencies.testing_robolectric) {
exclude group: 'org.apache.maven'
}
testImplementation 'org.apache.maven:maven-ant-tasks:2.1.3'
implementation FenixDependencies.mozilla_support_rusthttp
testImplementation ComponentsDependencies.testing_maven_ant_tasks
implementation project(':support-rusthttp')
androidTestImplementation FenixDependencies.mockk_android
testImplementation FenixDependencies.mockk
@ -698,14 +756,8 @@ dependencies {
}
protobuf {
// Mac M1 workaround until we can bump the version. Dependent on A-S.
// See https://github.com/mozilla-mobile/fenix/issues/22321
protoc {
if (osdetector.os == "osx") {
artifact = "${FenixDependencies.protobuf_compiler}:osx-x86_64"
} else {
artifact = FenixDependencies.protobuf_compiler
}
artifact = FenixDependencies.protobuf_compiler
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
@ -729,28 +781,28 @@ if (project.hasProperty("coverage")) {
}
jacoco {
toolVersion = "0.8.7"
toolVersion = Versions.jacoco
}
android.applicationVariants.all { variant ->
android.applicationVariants.configureEach { variant ->
tasks.register("jacoco${variant.name.capitalize()}TestReport", JacocoReport) {
dependsOn "test${variant.name.capitalize()}UnitTest"
reports {
xml.enabled = true
html.enabled = true
xml.required = true
html.required = true
}
def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*',
'**/*Test*.*', 'android/**/*.*', '**/*$[0-9].*']
def kotlinDebugTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.buildDir/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
def kotlinDebugTree = fileTree(dir: "$project.layout.buildDirectory/tmp/kotlin-classes/${variant.name}", excludes: fileFilter)
def javaDebugTree = fileTree(dir: "$project.layout.buildDirectory/intermediates/classes/${variant.flavorName}/${variant.buildType.name}",
excludes: fileFilter)
def mainSrc = "$project.projectDir/src/main/java"
sourceDirectories.setFrom(files([mainSrc]))
classDirectories.setFrom(files([kotlinDebugTree, javaDebugTree]))
executionData.setFrom(fileTree(dir: project.buildDir, includes: [
executionData.setFrom(fileTree(dir: project.layout.buildDirectory, includes: [
"jacoco/test${variant.name.capitalize()}UnitTest.exec",
'outputs/code-coverage/connected/*coverage.ec'
]))
@ -777,7 +829,7 @@ tasks.register('printVariants') {
doLast {
def variants = android.applicationVariants.collect { variant -> [
apks: variant.outputs.collect { output -> [
abi: output.getFilter(com.android.build.VariantOutput.FilterType.ABI),
abi: output.getFilter(FilterConfiguration.FilterType.ABI.name()),
fileName: output.outputFile.name
]},
build_type: variant.buildType.name,
@ -796,25 +848,6 @@ tasks.register('printVariants') {
}
}
task buildTranslationArray {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if(details.file.path.endsWith("${File.separator}strings.xml")){
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-','').replaceAll('-r','-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}','}')
android.defaultConfig.buildConfigField "String[]", "SUPPORTED_LOCALE_ARRAY", foundLocalesString
}
afterEvaluate {
// Format test output. Ported from AC #2401
@ -864,64 +897,16 @@ if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopsrcd
if (gradle.hasProperty('localProperties.dependencySubstitutions.geckoviewTopobjdir')) {
ext.topobjdir = gradle."localProperties.dependencySubstitutions.geckoviewTopobjdir"
}
ext.topsrcdir = gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir"
ext.topsrcdir = StringUtils.removeSuffix(gradle."localProperties.dependencySubstitutions.geckoviewTopsrcdir", File.separator)
apply from: "${topsrcdir}/substitute-local-geckoview.gradle"
}
def acSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.autoPublish.android-components.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.android-components.dir')) {
acSrcDir = gradle.getProperty('localProperties.branchBuild.android-components.dir')
}
if (acSrcDir) {
if (acSrcDir.startsWith("/")) {
apply from: "${acSrcDir}/substitute-local-ac.gradle"
} else {
apply from: "../${acSrcDir}/substitute-local-ac.gradle"
}
}
def appServicesSrcDir = null
if (gradle.hasProperty('localProperties.autoPublish.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.autoPublish.application-services.dir')
} else if (gradle.hasProperty('localProperties.branchBuild.application-services.dir')) {
appServicesSrcDir = gradle.getProperty('localProperties.branchBuild.application-services.dir')
}
if (appServicesSrcDir) {
if (appServicesSrcDir.startsWith("/")) {
apply from: "${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
} else {
apply from: "../${appServicesSrcDir}/build-scripts/substitute-local-appservices.gradle"
}
}
if (gradle.hasProperty('localProperties.autoPublish.glean.dir')) {
ext.gleanSrcDir = gradle."localProperties.autoPublish.glean.dir"
apply from: "../${gleanSrcDir}/build-scripts/substitute-local-glean.gradle"
}
// Define a reusable task for updating the versions of our built-in web extensions. We automate this
// to make sure we never forget to update the version, either in local development or for releases.
// In both cases, we want to make sure the latest version of all extensions (including their latest
// changes) are installed on first start-up.
// We're using the A-C version here as we want to uplift all built-in extensions to A-C (Once that's
// done we can also remove the task below):
// https://github.com/mozilla-mobile/android-components/issues/7249
ext.updateExtensionVersion = { task, extDir ->
configure(task) {
from extDir
include 'manifest.template.json'
rename { 'manifest.json' }
into extDir
def values = ['version': AndroidComponents.VERSION + "." + new Date().format('MMddHHmmss')]
inputs.properties(values)
expand(values)
}
}
android.applicationVariants.all { variant ->
android.applicationVariants.configureEach { variant ->
tasks.register("apkSize${variant.name.capitalize()}", ApkSizeTask) {
variantName = variant.name
apks = variant.outputs.collect { output -> output.outputFile.name }
@ -929,5 +914,24 @@ android.applicationVariants.all { variant ->
}
}
def getSupportedLocales() {
// This isn't running as a task, instead the array is build when the gradle file is parsed.
// https://github.com/mozilla-mobile/fenix/issues/14175
def foundLocales = new StringBuilder()
foundLocales.append("new String[]{")
fileTree("src/main/res").visit { FileVisitDetails details ->
if (details.file.path.endsWith("${File.separator}strings.xml")) {
def languageCode = details.file.parent.tokenize(File.separator).last().replaceAll('values-', '').replaceAll('-r', '-')
languageCode = (languageCode == "values") ? "en-US" : languageCode
foundLocales.append("\"").append(languageCode).append("\"").append(",")
}
}
foundLocales.append("}")
def foundLocalesString = foundLocales.toString().replaceAll(',}', '}')
return foundLocalesString
}
// Enable expiration by major version.
ext.gleanExpireByVersion = Config.majorVersion(project)
ext.gleanExpireByVersion = Config.majorVersion()

@ -1,4 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 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/. -->
<issues format="6" by="lint 7.0.0" type="baseline" client="gradle" name="AGP (7.0.0)" variant="all" version="7.0.0">
<issue
@ -1097,6 +1101,16 @@
column="1"/>
</issue>
<issue id="UnusedResources"
message="The resource R.drawable.ic_onboarding_key_features appears to be unused">
<location file="src/main/res/drawable/ic_onboarding_key_features.xml" />
</issue>
<issue id="UnusedResources"
message="The resource R.drawable.ic_onboarding_key_features_icons_only appears to be unused">
<location file="src/main/res/drawable/ic_onboarding_key_features_icons_only.xml" />
</issue>
<issue
id="IconXmlAndPng"
message="The following images appear both as density independent `.xml` files and as bitmap files: /Users/oracle/Projects/fenix/app/src/main/res/drawable-hdpi/ic_logo_wordmark_normal.png, /Users/oracle/Projects/fenix/app/src/main/res/drawable-night/ic_logo_wordmark_normal.xml">

@ -0,0 +1,53 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
---
# This file configures "evergreen" messages that are displayed via
# the Nimbus Messaging system.
#
# They are "evergreen" in that they apply to all users, and shipped with the app.
#
# This file is intended to grow new messages once messages have been tested via
# experiment, rolled out to everyone in the release, and are ready to be rolled out
# without the remote prompting from Experimenter.
#
# When adding new messages to this file, please add the experiment (and/or rollout) URLs used to
# validate them.
#
# Triggers, actions and styles are configured in messaging-fenix.fml.yaml.
import:
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
channel: release
features:
messaging:
# This message displays on the homescreen, asking the user to set Firefox as the default.
# It is triggered after a minimum of 4 launches of the app.
- value:
messages:
default-browser:
title: default_browser_experiment_card_title
text: default_browser_experiment_card_text
surface: homescreen
action: "MAKE_DEFAULT_BROWSER"
trigger:
- I_AM_NOT_DEFAULT_BROWSER
- USER_ESTABLISHED_INSTALL
style: PERSISTENT
button-label: preferences_set_as_default_browser
triggers:
USER_ESTABLISHED_INSTALL: "number_of_app_launches >=4"
# This message displays as a 'push' notification, asking the user to set Firefox as the default.
# It is triggered three days after install.
- value:
messages:
default-browser-notification:
title: nimbus_notification_default_browser_title
text: nimbus_notification_default_browser_text
surface: notification
style: NOTIFICATION
trigger:
- I_AM_NOT_DEFAULT_BROWSER
- DAY_3_AFTER_INSTALL
action: MAKE_DEFAULT_BROWSER

@ -0,0 +1,112 @@
# 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/.
---
includes:
- messaging-evergreen-messages.fml.yaml
import:
- path: ../android-components/components/service/nimbus/messaging.fml.yaml
channel: release
features:
messaging:
- value:
triggers:
# Using attributes built into the Nimbus SDK
USER_RECENTLY_INSTALLED: days_since_install < 7
USER_RECENTLY_UPDATED: days_since_update < 7 && days_since_install != days_since_update
USER_TIER_ONE_COUNTRY: ('US' in locale || 'GB' in locale || 'CA' in locale || 'DE' in locale || 'FR' in locale)
USER_EN_SPEAKER: "'en' in locale"
USER_ES_SPEAKER: "'es' in locale"
USER_DE_SPEAKER: "'de' in locale"
USER_FR_SPEAKER: "'fr' in locale"
DEVICE_ANDROID: os == 'Android'
DEVICE_IOS: os == 'iOS'
ALWAYS: "true"
NEVER: "false"
DAY_1_AFTER_INSTALL: days_since_install == 1
DAY_2_AFTER_INSTALL: days_since_install == 2
DAY_3_AFTER_INSTALL: days_since_install == 3
DAY_4_AFTER_INSTALL: days_since_install == 4
DAY_5_AFTER_INSTALL: days_since_install == 5
MORE_THAN_24H_SINCE_INSTALLED_OR_UPDATED: days_since_update >= 1
# Using custom attributes for the browser
I_AM_DEFAULT_BROWSER: "is_default_browser"
I_AM_NOT_DEFAULT_BROWSER: "is_default_browser == false"
FUNNEL_PAID: "adjust_campaign != ''"
FUNNEL_ORGANIC: "adjust_campaign == ''"
# Using Glean events, specific to the browser
INACTIVE_1_DAY: "'app_launched'|eventLastSeen('Hours') >= 24"
INACTIVE_2_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 2"
INACTIVE_3_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 3"
INACTIVE_4_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 4"
INACTIVE_5_DAYS: "'app_launched'|eventLastSeen('Days', 0) >= 5"
# Has the user signed in the last 4 years
FXA_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) <= 4"
FXA_NOT_SIGNED_IN: "'sync_auth.sign_in'|eventLastSeen('Years', 0) > 4"
# https://mozilla-hub.atlassian.net/wiki/spaces/FJT/pages/11469471/Core+Active
USER_INFREQUENT: "'app_launched'|eventCountNonZero('Days', 28) >= 1 && 'app_launched'|eventCountNonZero('Days', 28) < 7"
USER_CASUAL: "'app_launched'|eventCountNonZero('Days', 28) >= 7 && 'app_launched'|eventCountNonZero('Days', 28) < 14"
USER_REGULAR: "'app_launched'|eventCountNonZero('Days', 28) >= 14 && 'app_launched'|eventCountNonZero('Days', 28) < 21"
USER_CORE_ACTIVE: "'app_launched'|eventCountNonZero('Days', 28) >= 21"
LAUNCHED_ONCE_THIS_WEEK: "'app_launched'|eventSum('Days', 7) == 1"
actions:
ENABLE_PRIVATE_BROWSING: ://enable_private_browsing
INSTALL_SEARCH_WIDGET: ://install_search_widget
MAKE_DEFAULT_BROWSER: ://make_default_browser
VIEW_BOOKMARKS: ://urls_bookmarks
VIEW_COLLECTIONS: ://home_collections
VIEW_HISTORY: ://urls_history
VIEW_HOMESCREEN: ://home
OPEN_SETTINGS_ACCESSIBILITY: ://settings_accessibility
OPEN_SETTINGS_ADDON_MANAGER: ://settings_addon_manager
OPEN_SETTINGS_DELETE_BROWSING_DATA: ://settings_delete_browsing_data
OPEN_SETTINGS_LOGINS: ://settings_logins
OPEN_SETTINGS_NOTIFICATIONS: ://settings_notifications
OPEN_SETTINGS_PRIVACY: ://settings_privacy
OPEN_SETTINGS_SEARCH_ENGINE: ://settings_search_engine
OPEN_SETTINGS_TRACKING_PROTECTION: ://settings_tracking_protection
OPEN_SETTINGS_WALLPAPERS: ://settings_wallpapers
OPEN_SETTINGS: ://settings
TURN_ON_SYNC: ://turn_on_sync
styles:
DEFAULT:
priority: 50
max-display-count: 5
SURVEY:
priority: 55
max-display-count: 1
PERSISTENT:
priority: 50
max-display-count: 20
WARNING:
priority: 60
max-display-count: 10
URGENT:
priority: 100
max-display-count: 10
NOTIFICATION:
priority: 50
max-display-count: 1
$$surfaces:
- homescreen
- notification
- survey
- channel: developer
value:
styles:
DEFAULT:
priority: 50
max-display-count: 100
EXPIRES_QUICKLY:
priority: 100
max-display-count: 1
notification-config:
refresh-interval: 120 # minutes (2 hours)

File diff suppressed because it is too large Load Diff

@ -0,0 +1,469 @@
---
about:
description: Nimbus Feature Manifest for Fenix (Firefox Android)
kotlin:
package: org.mozilla.fenix
class: .nimbus.FxNimbus
channels:
- release
- beta
- nightly
- developer
- forkDebug
- forkRelease
includes:
- onboarding.fml.yaml
- pbm.fml.yaml
- messaging-fenix.fml.yaml
import:
- path: ../android-components/components/browser/engine-gecko/geckoview.fml.yaml
channel: release
features:
pdfjs:
- channel: developer
value: {
download-button: true,
open-in-app-button: true
}
- path: ../android-components/components/feature/fxsuggest/fxsuggest.fml.yaml
channel: release
features:
awesomebar-suggestion-provider:
- value:
available-suggestion-types: {
"amp": true,
"ampMobile": false,
"wikipedia": true,
}
features:
toolbar:
description: The searchbar/awesomebar that user uses to search.
variables:
toolbar-position-top:
description: If true, toolbar appears at top of the screen.
type: Boolean
default: false
homescreen:
description: The homescreen that the user goes to when they press home or new tab.
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled.
If the section is enabled, it should be toggleable in the settings screen, and on by default."
type: Map<HomeScreenSection, Boolean>
default:
{
"top-sites": true,
"jump-back-in": true,
"recently-saved": true,
"recent-explorations": true,
"pocket": true,
"pocket-sponsored-stories": true,
}
defaults:
- channel: nightly
value: {
"sections-enabled": {
"top-sites": true,
"jump-back-in": true,
"recently-saved": true,
"recent-explorations": true,
"pocket": true,
}
}
nimbus-validation:
description: "A feature that does not correspond to an application feature suitable for showing
that Nimbus is working. This should never be used in production."
variables:
settings-title:
description: The title of displayed in the Settings screen and app menu.
type: Text
default: browser_menu_settings
settings-punctuation:
description: The emoji displayed in the Settings screen title.
type: String
default: ""
settings-icon:
description: The drawable displayed in the app menu for Settings
type: String
default: mozac_ic_settings
search-term-groups:
description: A feature allowing the grouping of URLs around the search term that it came from.
variables:
enabled:
description: If true, the feature shows up on the homescreen and on the new tab screen.
type: Boolean
default: false
defaults:
- channel: nightly
value:
enabled: true
- channel: developer
value:
enabled: true
mr2022:
description: Features for MR 2022.
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled."
type: Map<MR2022Section, Boolean>
default:
{
"home-onboarding-dialog-existing-users": true,
"sync-cfr": true,
"wallpapers-selection-tool": true,
"jump-back-in-cfr": true,
"tcp-cfr": true,
"tcp-feature": true,
}
defaults:
- channel: developer
value: {
"sections-enabled": {
"home-onboarding-dialog-existing-users": true,
"sync-cfr": true,
"wallpapers-selection-tool": true,
"jump-back-in-cfr": true,
}
}
query-parameter-stripping:
description: Features for query parameter stripping.
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled."
type: Map<QueryParameterStrippingSection, String>
default:
{
"query-parameter-stripping": "0",
"query-parameter-stripping-pmb": "0",
"query-parameter-stripping-allow-list": "",
"query-parameter-stripping-strip-list": "",
}
defaults:
- channel: developer
value: {
"sections-enabled": {
"query-parameter-stripping": "0",
"query-parameter-stripping-pmb": "0",
"query-parameter-stripping-allow-list": "",
"query-parameter-stripping-strip-list": "",
}
}
- channel: nightly
value: {
"sections-enabled": {
"query-parameter-stripping": "0",
"query-parameter-stripping-pmb": "0",
"query-parameter-stripping-allow-list": "",
"query-parameter-stripping-strip-list": "",
}
}
cookie-banners:
description: Features for cookie banner handling.
variables:
sections-enabled:
description: "This property provides a lookup table of whether or not the given section should be enabled."
type: Map<CookieBannersSection, Int>
default:
{
"feature-ui": 0,
"feature-setting-value": 0,
"feature-setting-value-pbm": 0,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
defaults:
- channel: developer
value: {
"sections-enabled": {
"feature-ui": 1,
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
- channel: nightly
value: {
"sections-enabled": {
"feature-ui": 1,
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
- channel: beta
value: {
"sections-enabled": {
"feature-ui": 1,
"feature-setting-value": 0,
"feature-setting-value-pbm": 1,
"feature-setting-detect-only": 0,
"feature-setting-global-rules": 1,
"feature-setting-global-rules-sub-frames": 1,
}
}
unified-search:
description: A feature allowing user to easily search for specified results directly in the search bar.
variables:
enabled:
description: If true, the feature shows up in the search bar.
type: Boolean
default: true
growth-data:
description: A feature measuring campaign growth data
variables:
enabled:
description: If true, the feature is active
type: Boolean
default: false
defaults:
- channel: release
value:
enabled: true
re-engagement-notification:
description: A feature that shows the re-engagement notification if the user is inactive.
variables:
enabled:
description: If true, the re-engagement notification is shown to the inactive user.
type: Boolean
default: false
type:
description: The type of re-engagement notification that is shown to the inactive user.
type: Int
default: 0
onboarding:
description: "A feature that configures the new user onboarding page.
Note that onboarding is a **first run** feature, and should only be modified by first run experiments."
variables:
order:
description: Determines the order of the onboarding page panels
type: List<OnboardingPanel>
default: ["themes", "toolbar-placement", "sync", "tcp", "privacy-notice"]
glean:
description: "A feature that provides server-side configurations for Glean metrics (aka Server Knobs)."
variables:
metrics-enabled:
description: "A map of metric base-identifiers to booleans representing the state of the 'enabled' flag for that metric."
type: Map<String, Boolean>
default: {}
enable-event-timestamps:
description: "Enables precise event timestamps for Glean events"
type: Boolean
default: false
splash-screen:
description: "A feature that extends splash screen duration, allowing additional data fetching time for the app's initial run."
variables:
enabled:
description: "If true, the feature is active."
type: Boolean
default: false
maximum_duration_ms:
description: The maximum amount of time in milliseconds the splashscreen will be visible while waiting for initialization calls to complete.
type: Int
default: 0
shopping-experience:
description: A feature that shows product review quality information.
variables:
enabled:
description: if true, the shopping experience feature is shown to the user.
type: Boolean
default: false
product-recommendations:
description: if true, recommended products feature is enabled to be shown to the user based on their preference.
type: Boolean
default: false
product-recommendations-exposure:
description: if true, we want to record recommended products inventory for opted-in users, even if product recommendations are disabled.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
product-recommendations: true
product-recommendations-exposure: true
print:
description: A feature for printing from the share or browser menu.
variables:
share-print-enabled:
description: If true, a print button from the share menu is available.
type: Boolean
default: true
browser-print-enabled:
description: If true, a print button from the browser menu is available.
type: Boolean
default: true
search-extra-params:
description: A feature that provides additional args for search.
variables:
enabled:
description: If true, the feature is active.
type: Boolean
default: false
search-engine:
description: The search engine name.
type: String
default: ""
feature-enabler:
description: The feature enabler param name with arg, NOTE this map could be empty.
type: Map<String, String>
default: {}
channel-id:
description: The channel Id param name with arg.
type: Map<String, String>
default: {}
fx-strong-password:
description: A feature that provides a generated strong password on sign up.
variables:
enabled:
description: >
When the feature is enabled and Firefox receives a Login event with an
empty saved logins list, a suggested strong password prompt will be shown,
allowing the user to use the generated password to fill in the password field
for the new account that will be created. When the feature is disabled,
there won't be any prompt displayed that would allow using a generated password.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
- channel: nightly
value:
enabled: true
fx-suggest:
description: A feature that provides Firefox Suggest search suggestions.
variables:
enabled:
description: >
Whether the feature is enabled. When Firefox Suggest is enabled,
Firefox will download and store new search suggestions in the
background, and show additional Search settings to control which
suggestions appear in the awesomebar. When Firefox Suggest is
disabled, Firefox will not download new suggestions, and hide the
additional Search settings.
type: Boolean
default: false
defaults:
- channel: developer
value:
enabled: true
- channel: nightly
value:
enabled: true
nimbus-is-ready:
description: >
A feature that provides the number of Nimbus is_ready events to send
when Nimbus finishes launching.
variables:
event-count:
description: The number of events that should be sent.
type: Int
default: 1
types:
objects: {}
enums:
HomeScreenSection:
description: The identifiers for the sections of the homescreen.
variants:
top-sites:
description: The frecency and pinned sites.
recently-saved:
description: The sites the user has bookmarked recently.
jump-back-in:
description: The tabs the user was looking immediately before being interrupted.
recent-explorations:
description: The tab groups
pocket:
description: The pocket section. This should only be available in the US.
pocket-sponsored-stories:
description: Subsection of the Pocket homescreen section which shows sponsored stories.
MR2022Section:
description: The identifiers for the sections of the MR 2022.
variants:
home-onboarding-dialog-existing-users:
description: Home onboarding dialog for upgraded users.
sync-cfr:
description: CFR for the first time you see a synced tab on the home screen.
wallpapers-selection-tool:
description: Wallpapers selection dialog tool for the home screen.
jump-back-in-cfr:
description: Jump back-in onboarding message.
tcp-cfr:
description: CFR for the first time you use the browse with Total Cookie Protection on the browser screen.
tcp-feature:
description: Controls the Total Cookie Protection feature.
CookieBannersSection:
description: The identifiers for the sections of the MR 2022.
variants:
feature-ui:
description: An integer either 0 or 1 indicating if the UI for cookie banner handling should be visible,
0 to hide the UI and 1 to show the UI. The actual UI is composed by cookie banner section
in the settings page, the toolbar section and the re-engagement dialog.
feature-setting-value:
description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled,
0 for setting the value to disabled, 1 for enabling the setting with the value reject_all.
feature-setting-value-pbm:
description: An integer either 0 or 1 indicating if cookie banner setting should be enabled or disabled,
0 for setting the value to disabled, 1 for enabling the setting with the value reject_all.
feature-setting-detect-only:
description: An integer either 0 or 1 indicating if cookie banner detect only mode
should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting.
feature-setting-global-rules:
description: An integer either 0 or 1 indicating if cookie banner global rules
should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting.
feature-setting-global-rules-sub-frames:
description: An integer either 0 or 1 indicating if cookie banner global rules sub-frames
should be enabled or disabled. 0 for setting to be disabled, and 1 for enabling the setting.
QueryParameterStrippingSection:
description: The identifiers for the options for the Query Parameter Stripping feature.
variants:
query-parameter-stripping:
description: An integer either 0 or 1 indicating if query parameter stripping
should be enabled or disabled in normal mode. 0 for setting to be disabled,
and 1 for enabling the setting.
query-parameter-stripping-pmb:
description: An integer either 0 or 1 indicating if query parameter stripping
should be enabled or disabled in private mode. 0 for setting to be disabled,
and 1 for enabling the setting.
query-parameter-stripping-allow-list:
description: An string separated by commas indicating the sites where should
from query stripping should be exempted.
query-parameter-stripping-strip-list:
description: An string separated by commas indicating the list of query params
to be stripped from URIs. This list will be merged with records
coming from RemoteSettings.
OnboardingPanel:
description: The types of onboarding panels in the onboarding page
variants:
themes:
description: The themes onboarding panel where users pick themes
toolbar-placement:
description: The onboarding panel where users choose their toolbar placement (bottom or top)
sync:
description: The onboarding panel where users can sign in to sync
tcp:
description: The onboarding panel where users can choose their total cookie protection settings
privacy-notice:
description: The onboarding panel where users can tap to view our privacy notice.

@ -0,0 +1,132 @@
---
features:
juno-onboarding:
description: A feature that shows the onboarding flow.
variables:
conditions:
description: >
A collection of out the box conditional expressions to be
used in determining whether a card should show or not.
Each entry maps to a valid JEXL expression.
type: Map<ConditionName, String>
string-alias: ConditionName
default: {
ALWAYS: "true",
NEVER: "false"
}
cards:
description: Collection of user facing onboarding cards.
type: Map<OnboardingCardKey, OnboardingCardData>
string-alias: OnboardingCardKey
default:
default-browser:
card-type: default-browser
title: juno_onboarding_default_browser_title_nimbus_2
ordering: 10
body: juno_onboarding_default_browser_description_nimbus_3
image-res: ic_onboarding_welcome
primary-button-label: juno_onboarding_default_browser_positive_button
secondary-button-label: juno_onboarding_default_browser_negative_button
add-search-widget:
card-type: add-search-widget
enabled: false
title: juno_onboarding_add_search_widget_title
body: juno_onboarding_add_search_widget_description
image-res: ic_onboarding_search_widget
ordering: 15
primary-button-label: juno_onboarding_add_search_widget_positive_button
secondary-button-label: juno_onboarding_add_search_widget_negative_button
sync-sign-in:
card-type: sync-sign-in
title: juno_onboarding_sign_in_title_2
body: juno_onboarding_sign_in_description_2
image-res: ic_onboarding_sync
ordering: 20
primary-button-label: juno_onboarding_sign_in_positive_button
secondary-button-label: juno_onboarding_sign_in_negative_button
notification-permission:
card-type: notification-permission
title: juno_onboarding_enable_notifications_title_nimbus_2
body: juno_onboarding_enable_notifications_description_nimbus_2
image-res: ic_notification_permission
ordering: 30
primary-button-label: juno_onboarding_enable_notifications_positive_button
secondary-button-label: juno_onboarding_enable_notifications_negative_button
objects:
OnboardingCardData:
description: An object to describe a user facing onboarding card.
fields:
card-type:
type: OnboardingCardType
description: The type of the card.
# This should never be defaulted.
default: default-browser
enabled:
type: Boolean
description: If true, this card is shown to the user.
default: true
title:
type: Text
description: The title text displayed to the user.
# This should never be defaulted.
default: ""
body:
type: Text
description: The message text displayed to the user. May contain linkable text.
# This should never be defaulted.
default: ""
image-res:
type: Image
description: The resource id of the image to be displayed.
# This should never be defaulted.
default: ic_onboarding_welcome
ordering:
type: Int
description: Used to sequence the cards.
# This should never be defaulted.
default: 0
primary-button-label:
type: Text
description: The text to display on the primary button.
# This should never be defaulted.
default: ""
secondary-button-label:
type: Text
description: The text to display on the secondary button.
# This should never be defaulted.
default: ""
prerequisites:
type: List<ConditionName>
description: >
A list of strings corresponding to targeting expressions.
The card will be shown if all expressions are `true` and if
no expressions in the `disqualifiers` table are true, or
if the `disqualifiers` table is empty.
default: [ ALWAYS ]
disqualifiers:
type: List<ConditionName>
description: >
A list of strings corresponding to targeting expressions.
The card will not be shown if any expression is `true`.
default: [ NEVER ]
enums:
OnboardingCardType:
description: An enum to describe a type of card.
variants:
default-browser:
description: Allows user to set Firefox as the default browser.
sync-sign-in:
description: Allows user to sync with a Firefox account.
notification-permission:
description: Allows user to enable notification permission.
add-search-widget:
description: Allows user to add search widget to homescreen.

@ -0,0 +1,19 @@
---
features:
private-browsing:
description: Private Browsing Mode
variables:
felt-privacy-enabled:
description: if true, enable felt privacy related UI
type: Boolean
default: false
defaults:
- channel: developer
value:
felt-privacy-enabled: false
- channel: nightly
value:
felt-privacy-enabled: false

@ -21,9 +21,11 @@ activation:
first-session:
description: |
This ping is intended to capture the source of the app install
on the first session.
**THIS IS NOT A GENERIC FIRST USE PING** This ping is intended to capture
Adjust attribution. Use of this ping for other analyses will result in
undesirable outcomes.
include_client_id: true
send_if_empty: true
bugs:
- https://github.com/mozilla-mobile/fenix/issues/7295
data_reviews:
@ -62,3 +64,43 @@ spoc:
- https://github.com/mozilla-mobile/fenix/pull/27550#issuecomment-1295027631
notification_emails:
- android-probes@mozilla.com
cookie-banner-report-site:
description: |
This ping is needed when the cookie banner reducer doesn't work on
a website, and the user wants to report the site.
This ping doesn't include a client id.
include_client_id: false
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1805450
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/1298#pullrequestreview-1350344223
notification_emails:
- android-probes@mozilla.com
fx-suggest:
description: |
A ping representing a single event occurring with or to a Firefox Suggestion.
Distinguishable by its `ping_type`.
Does not contain a `client_id`, preferring a `context_id` instead.
include_client_id: false
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1857092
data_reviews:
- https://github.com/mozilla-mobile/firefox-android/pull/3958#issuecomment-1758607927
notification_emails:
- lina@mozilla.com
- ttran@mozilla.com
- najiang@mozilla.com
font-list:
description: |
List of fonts installed on the user's device
include_client_id: false
bugs:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193
data_reviews:
- https://bugzilla.mozilla.org/show_bug.cgi?id=1858193#c2
notification_emails:
- android-probes@mozilla.com
- tom@mozilla.com

@ -1,26 +1,5 @@
-dontobfuscate
####################################################################################################
# Sentry
####################################################################################################
# Recommended config via https://docs.sentry.io/clients/java/modules/android/#manual-integration
# Since we don't obfuscate, we don't need to use their Gradle plugin to upload ProGuard mappings.
-keepattributes LineNumberTable,SourceFile
-dontwarn org.slf4j.**
-dontwarn javax.**
# Our addition: this class is saved to disk via Serializable, which ProGuard doesn't like.
# If we exclude this, upload silently fails (Sentry swallows a NPE so we don't crash).
# I filed https://github.com/getsentry/sentry-java/issues/572
#
# If Sentry ever mysteriously stops working after we upgrade it, this could be why.
-keep class io.sentry.event.Event { *; }
# Iceraven release build mysteriously cannot find a lot of Sentry.
# Luckily we do not really want it anyway.
-dontwarn io.sentry.**
####################################################################################################
# Android and GeckoView built-ins
####################################################################################################
@ -73,58 +52,12 @@
-keep class org.mozilla.fenix.**ViewModel { *; }
####################################################################################################
# Adjust
####################################################################################################
-keep public class com.adjust.sdk.** { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
-keep public class com.android.installreferrer.** { *; }
-keep class dalvik.system.VMRuntime {
java.lang.String getRuntime();
}
-keep class android.os.Build {
java.lang.String[] SUPPORTED_ABIS;
java.lang.String CPU_ABI;
}
-keep class android.content.res.Configuration {
android.os.LocaledList getLocales();
java.util.Locale locale;
}
-keep class android.os.LocaleList {
java.util.Locale get(int);
}
# Keep code generated from Glean Metrics
-keep class org.mozilla.fenix.GleanMetrics.** { *; }
# Keep motionlayout internal methods
# https://github.com/mozilla-mobile/fenix/issues/2094
-keep class androidx.constraintlayout.** { *; }
# Keep adjust relevant classes
-keep class com.adjust.sdk.** { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
-keep public class com.android.installreferrer.** { *; }
# Keep Android Lifecycle methods
# https://bugzilla.mozilla.org/show_bug.cgi?id=1596302
-keep class androidx.lifecycle.** { *; }
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.FeatureDescriptor
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor

@ -5,12 +5,13 @@
</head>
<body>
<form>
<p>Street Address: <input id="streetAddress" type="text"></p>
<p>City: <input id="city" type="text"></p>
<p>Zip Code: <input id="zipCode" type="text"></p>
<p>Country: <input id="country" type="text"></p>
<p>Telephone: <input id="telephone" type="text"></p>
<p>Email: <input id="email" type="text"></p>
<p>Street Address: <input id="streetAddress" type="text"></p>
<p>City: <input id="city" type="text"></p>
<p>Zip Code: <input id="zipCode" type="text"></p>
<p>Country: <input id="country" type="text"></p>
<p>Telephone: <input id="telephone" type="text"></p>
<p>Email: <input id="email" type="text"></p>
<p>Apartment, suite, etc. <input id="apartment" type="text"></p>
</form>
</body>
</html>
</html>

@ -10,4 +10,4 @@
</audio>
</div>
</body>
</html>
</html>

@ -5,9 +5,13 @@
</head>
<body>
<form>
<p>Card information</p>
<p>Card Number: <input id="cardNumber" type="text" placeholder="1234 1234 1234 1234"></p>
<p>Name on card: <input id="nameOnCard"type="text" placeholder="Name on card"></p>
<p>Card information</p>
<p>Card Number: <input id="cardNumber" type="text" placeholder="1234 1234 1234 1234"></p>
<p>Name on card: <input id="nameOnCard"type="text" placeholder="Name on card"></p>
<p> Expiry date:
<input id="expiryMonthAndYear" inputmode="numerical" placeholder="MM / YYYY" type="text" />
</p>
<p><input type="submit" id="submit" value="Submit" aria-label="submit"/></p>
</form>
</body>
</html>
</html>

@ -1,4 +1,11 @@
<html>
<!-- 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/. -->
<!-- This asset is using the code behind
- https://www.mozilla-anti-tracking.com/test/dfpi/storage_access_api.html
- test page.
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
<body>
<h2>Cross-site cookies storage access test</h2>
<h3>anti-tracker-test.com</h3>

@ -8,15 +8,22 @@
<p>Misc Link Types</p>
<section>
<a href="https://duckduckgo.com/">External link</a>
<a href="mailto://example@example.com">Email link</a>
</section>
<section>
<a href="mailto://example@example.com">Email link</a>
<a href="tel://1234567890">Telephone link</a>
</section>
<section>
<a href="tel://1234567890">Telephone link</a>
<a href="vnd.youtube://@Mozilla">Youtube schema link</a>
</section>
<section>
<a href="https://m.youtube.com/user/mozilla?cbrd=1">Youtube full link</a>
</section>
<section>
<a href="http://play.google.com/store/apps/details?id=org.mozilla.firefox">Playstore link</a>
</section>
</html>

@ -7,4 +7,4 @@
<p id="testContent">Page content: 1</p>
</h1>
</body>
</html>
</html>

@ -7,4 +7,4 @@
<p id="testContent">Page content: 2</p>
</h1>
</body>
</html>
</html>

@ -8,6 +8,9 @@
<p>
<a href="https://play.google.com/store/apps/details?id=org.mozilla.fenix">Mozilla Playstore link</a>
</p>
<p>
<a href="../resources/pdfForm.pdf">PDF form file</a>
</p>
</h1>
</body>
</html>
</html>

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width">
<body>
<script type = "text/javascript" >
const gpcValue = navigator.globalPrivacyControl
if (gpcValue) {
document.write('<p>GPC is enabled.</p>');
} else {
document.write('<p>GPC not enabled.</p>');
}
</script>
</body>
</html>

@ -35,6 +35,15 @@
<button onclick="printOption()" id="submitOption"> Submit drop down option </button>
<div id="displayOption"></div>
</br>
<form method="post" enctype="multipart/form-data">
<div>
<label>Choose file to upload</label>
</br>
<input type="file" id="upload_file"/>
</div>
</form>
<script>
function printOption() {
let dropDown = document.querySelector("#dropDown");

@ -0,0 +1,49 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html>
<head>
<title>Muted_Video_Test_Page</title>
</head>
<body>
<p id="testContent">Page content: muted video player</p>
<div class="playbackState">
<p>Media file not playing</p>
</div>
<div id="video-container" style="text-align:center">
<button onclick="play()">Play</button>
<button onclick="pause()">Pause</button>
<button onclick="fullscreen()">Full Screen</button>
<br><br>
<video id="mutedVideo" width="420" autoplay muted controls loop>
<source src="../resources/clip.mp4" type="video/mp4">
Your browser does not support HTML video.
</video>
</div>
<script>
const mutedVideo = document.getElementById("mutedVideo");
function play() {
mutedVideo.play();
}
function pause() {
mutedVideo.pause();
}
function fullscreen() {
mutedVideo.requestFullscreen();
}
mutedVideo.addEventListener('playing', (event) => {
document.querySelector('.playbackState').innerHTML="Media file is playing";
});
mutedVideo.addEventListener('pause', (event) => {
document.querySelector('.playbackState').innerHTML="Media file is paused";
});
</script>
</body>
</html>

@ -20,4 +20,4 @@
</script>
</body>
</html>
</html>

@ -25,4 +25,4 @@
</script>
</body>
</html>
</html>

@ -2,14 +2,16 @@
<!-- 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/. -->
<!-- This asset is using the code behind
- https://www.mozilla-anti-tracking.com/test/trackingprotection/test_pages/tracking_protection.html
- test page.
- Source repository: https://github.com/mozilla/anti-tracking-test-pages -->
<html dir="ltr" xml:lang="en-US" lang="en-US">
<head>
<meta charset="utf8">
<script src="../resources/trackingAPI.js" type="text/javascript"></script>
</head>
<body>
<iframe src="http://trackertest.org/"></iframe>
<h3>Level 1 (Basic) List</h3>
<p>social-track-digest256:</p>
<img
@ -18,19 +20,19 @@
<br/>
<p>ads-track-digest256:</p>
<img
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png"
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png" alt="ads not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='ads blocked'">
<br/>
<p>analytics-track-digest256:</p>
<img
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png"
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png" alt="analytics not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='analytics blocked'">
<br/>
<p>Fingerprinting:
<pre id="result">test not run</pre>
<script src="https://base-fingerprinting-track-digest256.dummytracker.org/tracker.js"
onerror="this.onerror=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting NOT blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='Fingerprinting not blocked';"
></script>
</p>
<br/>

@ -1,14 +1,49 @@
<!DOCTYPE HTML>
<!-- 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/. -->
<html>
<head>
<title>Video_Test_Page</title>
<meta name="viewport" content="width=device-width">
</head>
<body>
<p id="testContent">Page content: video player</p>
<div id="video-container">
<video id="videoSample" width="320" height="240" controls loop>
<source src="../resources/clip.mp4">
<div class="playbackState">
<p>Media file not playing</p>
</div>
<div id="video-container" style="text-align:center">
<button onclick="play()">Play</button>
<button onclick="pause()">Pause</button>
<button onclick="fullscreen()">Full Screen</button>
<br><br>
<video id="video" width="420" autoplay controls loop>
<source src="../resources/clip.mp4" type="video/mp4">
Your browser does not support HTML video.
</video>
</div>
<script>
const video = document.getElementById("video");
function play() {
video.play();
}
function pause() {
video.pause();
}
function fullscreen() {
video.requestFullscreen();
}
video.addEventListener('playing', (event) => {
document.querySelector('.playbackState').innerHTML="Media file is playing";
});
video.addEventListener('pause', (event) => {
document.querySelector('.playbackState').innerHTML="Media file is paused";
});
</script>
</body>
</html>
</html>

@ -0,0 +1,23 @@
From: https://raw.githubusercontent.com/fonttools/fonttools/main/LICENSE
MIT License
Copyright (c) 2017 Just van Rossum
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -23,7 +23,9 @@ import java.lang.ref.WeakReference
* deactivate the FxA web channel
* which is not supported on the staging servers.
*/
class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
class AppRequestInterceptor(
private val context: Context,
) : RequestInterceptor {
private var navController: WeakReference<NavController>? = null
@ -53,11 +55,6 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
)?.let { response ->
return response
}
interceptAmoRequest(uri, isSameDomain, hasUserGesture)?.let { response ->
return response
}
return context.components.services.appLinksInterceptor
.onLoadRequest(
engineSession,
@ -86,46 +83,15 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
errorType = improvedErrorType,
uri = uri,
htmlResource = riskLevel.htmlRes,
titleOverride = { type -> getErrorPageTitle(context, type) },
descriptionOverride = { type -> getErrorPageDescription(context, type) },
)
return RequestInterceptor.ErrorResponse(errorPageUri)
}
/**
* Checks if the provided [uri] is a request to install an add-on from addons.mozilla.org and
* redirects to Add-ons Manager to trigger installation if needed.
*
* @return [RequestInterceptor.InterceptionResponse.Deny] when installation was triggered and
* the original request can be skipped, otherwise null to continue loading the page.
*/
private fun interceptAmoRequest(
uri: String,
isSameDomain: Boolean,
hasUserGesture: Boolean,
): RequestInterceptor.InterceptionResponse? {
// First we execute a quick check to see if this is a request we're interested in i.e. a
// request triggered by the user and coming from AMO.
if (hasUserGesture && isSameDomain && uri.startsWith(AMO_BASE_URL)) {
// Check if this is a request to install an add-on.
val matchResult = AMO_INSTALL_URL_REGEX.toRegex().matchEntire(uri)
if (matchResult != null) {
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId),
)
// We've redirected to the add-ons management fragment, skip original request.
return RequestInterceptor.InterceptionResponse.Deny
}
}
}
// In all other case we let the original request proceed.
return null
}
@Suppress("LongParameterList")
// This method is the only difference from the production code.
// Otherwise the code should be kept identical
private fun interceptFxaRequest(
engineSession: EngineSession,
uri: String,
@ -160,6 +126,7 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
return when {
errorType == ErrorType.ERROR_UNKNOWN_HOST && !isConnected -> ErrorType.ERROR_NO_INTERNET
errorType == ErrorType.ERROR_HTTPS_ONLY -> ErrorType.ERROR_HTTPS_ONLY
else -> errorType
}
}
@ -201,6 +168,25 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
-> RiskLevel.High
}
private fun getErrorPageTitle(context: Context, type: ErrorType): String? {
return when (type) {
ErrorType.ERROR_HTTPS_ONLY -> context.getString(R.string.errorpage_httpsonly_title)
// Returning `null` will let the component use its default title for this error type
else -> null
}
}
private fun getErrorPageDescription(context: Context, type: ErrorType): String? {
return when (type) {
ErrorType.ERROR_HTTPS_ONLY ->
context.getString(R.string.errorpage_httpsonly_message_title) +
"<br><br>" +
context.getString(R.string.errorpage_httpsonly_message_summary)
// Returning `null` will let the component use its default description for this error type
else -> null
}
}
internal enum class RiskLevel(val htmlRes: String) {
Low(LOW_AND_MEDIUM_RISK_ERROR_PAGES),
Medium(LOW_AND_MEDIUM_RISK_ERROR_PAGES),
@ -210,7 +196,5 @@ class AppRequestInterceptor(private val context: Context) : RequestInterceptor {
companion object {
internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html"
internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html"
internal const val AMO_BASE_URL = BuildConfig.AMO_BASE_URL
internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)"
}
}

@ -0,0 +1,79 @@
package org.mozilla.fenix.components
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.mozilla.fenix.components.metrics.fonts.FontEnumerationWorker
import org.mozilla.fenix.components.metrics.fonts.FontParser
class FontParserTest {
@Test
fun testSanityAssertion() {
/*
Changing the below constant causes _all_ Nightly users to send a (large) Telemetry event containing
their font information. Do not change this value unless you explicitly intend this.
*/
assertEquals(4, FontEnumerationWorker.kDesiredSubmissions)
}
@Test
fun testFontParsing() {
val assetManager = InstrumentationRegistry.getInstrumentation().context.assets
val font1 = FontParser.parse("no-path", assetManager.open("resources/TestTTF.ttf"))
assertEquals(
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
"\u0000T\u0000F",
font1.family,
)
assertEquals(
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
"\u00000\u00000\u00000",
font1.fontVersion,
)
assertEquals(
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
font1.fullName,
)
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
assertEquals(
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
font1.uniqueSubFamily,
)
assertEquals(
"C4E8CE309F44A131D061D73B2580E922A7F5ECC8D7109797AC0FF58BF8723B7B",
font1.hash,
)
assertEquals(3516272951, font1.created)
assertEquals(3573411749, font1.modified)
assertEquals(65536, font1.revision)
val font2 = FontParser.parse("no-path", assetManager.open("resources/TestTTC.ttc"))
assertEquals(
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T" +
"\u0000T\u0000F",
font2.family,
)
assertEquals(
"\u0000V\u0000e\u0000r\u0000s\u0000i\u0000o\u0000n\u0000 \u00001\u0000." +
"\u00000\u00000\u00000",
font2.fontVersion,
)
assertEquals(
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F",
font2.fullName,
)
assertEquals("\u0000R\u0000e\u0000g\u0000u\u0000l\u0000a\u0000r", font1.subFamily)
assertEquals(
"\u0000F\u0000o\u0000n\u0000t\u0000T\u0000o\u0000o\u0000l\u0000s\u0000:\u0000 " +
"\u0000T\u0000e\u0000s\u0000t\u0000 \u0000T\u0000T\u0000F\u0000:\u0000 \u00002\u00000\u00001\u00005",
font2.uniqueSubFamily,
)
assertEquals(
"A8521588045ED5F1F8B07EECAAC06ED3186C644655BFAC00DD4507CD316FBDC5",
font2.hash,
)
assertEquals(3516272951, font2.created)
assertEquals(3573411749, font2.modified)
assertEquals(65536, font2.revision)
}
}

@ -5,8 +5,8 @@
package org.mozilla.fenix.components
import android.content.Context
import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.ServerConfig.Server
import mozilla.appservices.fxaclient.FxaConfig
import mozilla.appservices.fxaclient.FxaServer
/**
* Utility to configure Firefox Account stage servers.
@ -17,7 +17,7 @@ object FxaServer {
private const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
@Suppress("UNUSED_PARAMETER")
fun config(context: Context): ServerConfig {
return ServerConfig(Server.STAGE, CLIENT_ID, REDIRECT_URL)
fun config(context: Context): FxaConfig {
return FxaConfig(FxaServer.Stage, CLIENT_ID, REDIRECT_URL)
}
}

@ -0,0 +1,75 @@
/* 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.experimentintegration
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.homeScreen
class GenericExperimentIntegrationTest {
private val experimentName = "Viewpoint"
@get:Rule
val activityTestRule = HomeActivityTestRule(
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
)
@Before
fun setUp() {
TestHelper.appContext.settings().showSecretDebugMenuThisSession = true
}
@After
fun tearDown() {
TestHelper.appContext.settings().showSecretDebugMenuThisSession = false
}
@Test
fun disableStudiesViaStudiesToggle() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openExperimentsMenu {
verifyExperimentEnrolled(experimentName)
}.goBack {
}.openSettingsSubMenuDataCollection {
clickStudiesOption()
verifyStudiesToggle(true)
clickStudiesToggle()
clickStudiesDialogOkButton()
}
}
@Test
fun testExperimentUnenrolled() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openExperimentsMenu {
verifyExperimentExists(experimentName)
verifyExperimentNotEnrolled(experimentName)
}
}
@Test
fun testExperimentUnenrolledViaSecretMenu() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openExperimentsMenu {
verifyExperimentExists(experimentName)
verifyExperimentEnrolled(experimentName)
unenrollfromExperiment(experimentName)
verifyExperimentNotEnrolled(experimentName)
}
}
}

@ -0,0 +1,24 @@
# 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/.
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
pydantic = "*"
pytest = "*"
pytest-html = "*"
pytest-metadata = "*"
pytest-variables = "*"
pyyaml = "*"
requests = "*"
[dev-packages]
black = "*"
flake8 = "*"
[requires]
python_version = "3.11"

@ -0,0 +1,578 @@
{
"_meta": {
"hash": {
"sha256": "6dae5ac51aa7817578a25597da1ef783475050538443ba344c88a78969e68fd9"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.11"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"annotated-types": {
"hashes": [
"sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43",
"sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"
],
"markers": "python_version >= '3.8'",
"version": "==0.6.0"
},
"certifi": {
"hashes": [
"sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082",
"sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"
],
"markers": "python_version >= '3.6'",
"version": "==2023.7.22"
},
"charset-normalizer": {
"hashes": [
"sha256:02673e456dc5ab13659f85196c534dc596d4ef260e4d86e856c3b2773ce09843",
"sha256:02af06682e3590ab952599fbadac535ede5d60d78848e555aa58d0c0abbde786",
"sha256:03680bb39035fbcffe828eae9c3f8afc0428c91d38e7d61aa992ef7a59fb120e",
"sha256:0570d21da019941634a531444364f2482e8db0b3425fcd5ac0c36565a64142c8",
"sha256:09c77f964f351a7369cc343911e0df63e762e42bac24cd7d18525961c81754f4",
"sha256:0d3d5b7db9ed8a2b11a774db2bbea7ba1884430a205dbd54a32d61d7c2a190fa",
"sha256:1063da2c85b95f2d1a430f1c33b55c9c17ffaf5e612e10aeaad641c55a9e2b9d",
"sha256:12ebea541c44fdc88ccb794a13fe861cc5e35d64ed689513a5c03d05b53b7c82",
"sha256:153e7b6e724761741e0974fc4dcd406d35ba70b92bfe3fedcb497226c93b9da7",
"sha256:15b26ddf78d57f1d143bdf32e820fd8935d36abe8a25eb9ec0b5a71c82eb3895",
"sha256:1872d01ac8c618a8da634e232f24793883d6e456a66593135aeafe3784b0848d",
"sha256:187d18082694a29005ba2944c882344b6748d5be69e3a89bf3cc9d878e548d5a",
"sha256:1b2919306936ac6efb3aed1fbf81039f7087ddadb3160882a57ee2ff74fd2382",
"sha256:232ac332403e37e4a03d209a3f92ed9071f7d3dbda70e2a5e9cff1c4ba9f0678",
"sha256:23e8565ab7ff33218530bc817922fae827420f143479b753104ab801145b1d5b",
"sha256:24817cb02cbef7cd499f7c9a2735286b4782bd47a5b3516a0e84c50eab44b98e",
"sha256:249c6470a2b60935bafd1d1d13cd613f8cd8388d53461c67397ee6a0f5dce741",
"sha256:24a91a981f185721542a0b7c92e9054b7ab4fea0508a795846bc5b0abf8118d4",
"sha256:2502dd2a736c879c0f0d3e2161e74d9907231e25d35794584b1ca5284e43f596",
"sha256:250c9eb0f4600361dd80d46112213dff2286231d92d3e52af1e5a6083d10cad9",
"sha256:278c296c6f96fa686d74eb449ea1697f3c03dc28b75f873b65b5201806346a69",
"sha256:2935ffc78db9645cb2086c2f8f4cfd23d9b73cc0dc80334bc30aac6f03f68f8c",
"sha256:2f4a0033ce9a76e391542c182f0d48d084855b5fcba5010f707c8e8c34663d77",
"sha256:30a85aed0b864ac88309b7d94be09f6046c834ef60762a8833b660139cfbad13",
"sha256:380c4bde80bce25c6e4f77b19386f5ec9db230df9f2f2ac1e5ad7af2caa70459",
"sha256:3ae38d325b512f63f8da31f826e6cb6c367336f95e418137286ba362925c877e",
"sha256:3b447982ad46348c02cb90d230b75ac34e9886273df3a93eec0539308a6296d7",
"sha256:3debd1150027933210c2fc321527c2299118aa929c2f5a0a80ab6953e3bd1908",
"sha256:4162918ef3098851fcd8a628bf9b6a98d10c380725df9e04caf5ca6dd48c847a",
"sha256:468d2a840567b13a590e67dd276c570f8de00ed767ecc611994c301d0f8c014f",
"sha256:4cc152c5dd831641e995764f9f0b6589519f6f5123258ccaca8c6d34572fefa8",
"sha256:542da1178c1c6af8873e143910e2269add130a299c9106eef2594e15dae5e482",
"sha256:557b21a44ceac6c6b9773bc65aa1b4cc3e248a5ad2f5b914b91579a32e22204d",
"sha256:5707a746c6083a3a74b46b3a631d78d129edab06195a92a8ece755aac25a3f3d",
"sha256:588245972aca710b5b68802c8cad9edaa98589b1b42ad2b53accd6910dad3545",
"sha256:5adf257bd58c1b8632046bbe43ee38c04e1038e9d37de9c57a94d6bd6ce5da34",
"sha256:619d1c96099be5823db34fe89e2582b336b5b074a7f47f819d6b3a57ff7bdb86",
"sha256:63563193aec44bce707e0c5ca64ff69fa72ed7cf34ce6e11d5127555756fd2f6",
"sha256:67b8cc9574bb518ec76dc8e705d4c39ae78bb96237cb533edac149352c1f39fe",
"sha256:6a685067d05e46641d5d1623d7c7fdf15a357546cbb2f71b0ebde91b175ffc3e",
"sha256:70f1d09c0d7748b73290b29219e854b3207aea922f839437870d8cc2168e31cc",
"sha256:750b446b2ffce1739e8578576092179160f6d26bd5e23eb1789c4d64d5af7dc7",
"sha256:7966951325782121e67c81299a031f4c115615e68046f79b85856b86ebffc4cd",
"sha256:7b8b8bf1189b3ba9b8de5c8db4d541b406611a71a955bbbd7385bbc45fcb786c",
"sha256:7f5d10bae5d78e4551b7be7a9b29643a95aded9d0f602aa2ba584f0388e7a557",
"sha256:805dfea4ca10411a5296bcc75638017215a93ffb584c9e344731eef0dcfb026a",
"sha256:81bf654678e575403736b85ba3a7867e31c2c30a69bc57fe88e3ace52fb17b89",
"sha256:82eb849f085624f6a607538ee7b83a6d8126df6d2f7d3b319cb837b289123078",
"sha256:85a32721ddde63c9df9ebb0d2045b9691d9750cb139c161c80e500d210f5e26e",
"sha256:86d1f65ac145e2c9ed71d8ffb1905e9bba3a91ae29ba55b4c46ae6fc31d7c0d4",
"sha256:86f63face3a527284f7bb8a9d4f78988e3c06823f7bea2bd6f0e0e9298ca0403",
"sha256:8eaf82f0eccd1505cf39a45a6bd0a8cf1c70dcfc30dba338207a969d91b965c0",
"sha256:93aa7eef6ee71c629b51ef873991d6911b906d7312c6e8e99790c0f33c576f89",
"sha256:96c2b49eb6a72c0e4991d62406e365d87067ca14c1a729a870d22354e6f68115",
"sha256:9cf3126b85822c4e53aa28c7ec9869b924d6fcfb76e77a45c44b83d91afd74f9",
"sha256:9fe359b2e3a7729010060fbca442ca225280c16e923b37db0e955ac2a2b72a05",
"sha256:a0ac5e7015a5920cfce654c06618ec40c33e12801711da6b4258af59a8eff00a",
"sha256:a3f93dab657839dfa61025056606600a11d0b696d79386f974e459a3fbc568ec",
"sha256:a4b71f4d1765639372a3b32d2638197f5cd5221b19531f9245fcc9ee62d38f56",
"sha256:aae32c93e0f64469f74ccc730a7cb21c7610af3a775157e50bbd38f816536b38",
"sha256:aaf7b34c5bc56b38c931a54f7952f1ff0ae77a2e82496583b247f7c969eb1479",
"sha256:abecce40dfebbfa6abf8e324e1860092eeca6f7375c8c4e655a8afb61af58f2c",
"sha256:abf0d9f45ea5fb95051c8bfe43cb40cda383772f7e5023a83cc481ca2604d74e",
"sha256:ac71b2977fb90c35d41c9453116e283fac47bb9096ad917b8819ca8b943abecd",
"sha256:ada214c6fa40f8d800e575de6b91a40d0548139e5dc457d2ebb61470abf50186",
"sha256:b09719a17a2301178fac4470d54b1680b18a5048b481cb8890e1ef820cb80455",
"sha256:b1121de0e9d6e6ca08289583d7491e7fcb18a439305b34a30b20d8215922d43c",
"sha256:b3b2316b25644b23b54a6f6401074cebcecd1244c0b8e80111c9a3f1c8e83d65",
"sha256:b3d9b48ee6e3967b7901c052b670c7dda6deb812c309439adaffdec55c6d7b78",
"sha256:b5bcf60a228acae568e9911f410f9d9e0d43197d030ae5799e20dca8df588287",
"sha256:b8f3307af845803fb0b060ab76cf6dd3a13adc15b6b451f54281d25911eb92df",
"sha256:c2af80fb58f0f24b3f3adcb9148e6203fa67dd3f61c4af146ecad033024dde43",
"sha256:c350354efb159b8767a6244c166f66e67506e06c8924ed74669b2c70bc8735b1",
"sha256:c5a74c359b2d47d26cdbbc7845e9662d6b08a1e915eb015d044729e92e7050b7",
"sha256:c71f16da1ed8949774ef79f4a0260d28b83b3a50c6576f8f4f0288d109777989",
"sha256:d47ecf253780c90ee181d4d871cd655a789da937454045b17b5798da9393901a",
"sha256:d7eff0f27edc5afa9e405f7165f85a6d782d308f3b6b9d96016c010597958e63",
"sha256:d97d85fa63f315a8bdaba2af9a6a686e0eceab77b3089af45133252618e70884",
"sha256:db756e48f9c5c607b5e33dd36b1d5872d0422e960145b08ab0ec7fd420e9d649",
"sha256:dc45229747b67ffc441b3de2f3ae5e62877a282ea828a5bdb67883c4ee4a8810",
"sha256:e0fc42822278451bc13a2e8626cf2218ba570f27856b536e00cfa53099724828",
"sha256:e39c7eb31e3f5b1f88caff88bcff1b7f8334975b46f6ac6e9fc725d829bc35d4",
"sha256:e46cd37076971c1040fc8c41273a8b3e2c624ce4f2be3f5dfcb7a430c1d3acc2",
"sha256:e5c1502d4ace69a179305abb3f0bb6141cbe4714bc9b31d427329a95acfc8bdd",
"sha256:edfe077ab09442d4ef3c52cb1f9dab89bff02f4524afc0acf2d46be17dc479f5",
"sha256:effe5406c9bd748a871dbcaf3ac69167c38d72db8c9baf3ff954c344f31c4cbe",
"sha256:f0d1e3732768fecb052d90d62b220af62ead5748ac51ef61e7b32c266cac9293",
"sha256:f5969baeaea61c97efa706b9b107dcba02784b1601c74ac84f2a532ea079403e",
"sha256:f8888e31e3a85943743f8fc15e71536bda1c81d5aa36d014a3c0c44481d7db6e",
"sha256:fc52b79d83a3fe3a360902d3f5d79073a993597d48114c29485e9431092905d8"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==3.3.0"
},
"idna": {
"hashes": [
"sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4",
"sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"
],
"markers": "python_version >= '3.5'",
"version": "==3.4"
},
"iniconfig": {
"hashes": [
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"jinja2": {
"hashes": [
"sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa",
"sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.1.3"
},
"markupsafe": {
"hashes": [
"sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e",
"sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e",
"sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431",
"sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686",
"sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c",
"sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559",
"sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc",
"sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb",
"sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939",
"sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c",
"sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0",
"sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4",
"sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9",
"sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575",
"sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba",
"sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d",
"sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd",
"sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3",
"sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00",
"sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155",
"sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac",
"sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52",
"sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f",
"sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8",
"sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b",
"sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007",
"sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24",
"sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea",
"sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198",
"sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0",
"sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee",
"sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be",
"sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2",
"sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1",
"sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707",
"sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6",
"sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c",
"sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58",
"sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823",
"sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779",
"sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636",
"sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c",
"sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad",
"sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee",
"sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc",
"sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2",
"sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48",
"sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7",
"sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e",
"sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b",
"sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa",
"sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5",
"sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e",
"sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb",
"sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9",
"sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57",
"sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc",
"sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc",
"sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2",
"sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.3"
},
"packaging": {
"hashes": [
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
],
"markers": "python_version >= '3.7'",
"version": "==23.2"
},
"pluggy": {
"hashes": [
"sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
"sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
],
"markers": "python_version >= '3.8'",
"version": "==1.3.0"
},
"pydantic": {
"hashes": [
"sha256:94f336138093a5d7f426aac732dcfe7ab4eb4da243c88f891d65deb4a2556ee7",
"sha256:bc3ddf669d234f4220e6e1c4d96b061abe0998185a8d7855c0126782b7abc8c1"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.4.2"
},
"pydantic-core": {
"hashes": [
"sha256:042462d8d6ba707fd3ce9649e7bf268633a41018d6a998fb5fbacb7e928a183e",
"sha256:0523aeb76e03f753b58be33b26540880bac5aa54422e4462404c432230543f33",
"sha256:05560ab976012bf40f25d5225a58bfa649bb897b87192a36c6fef1ab132540d7",
"sha256:0675ba5d22de54d07bccde38997e780044dcfa9a71aac9fd7d4d7a1d2e3e65f7",
"sha256:073d4a470b195d2b2245d0343569aac7e979d3a0dcce6c7d2af6d8a920ad0bea",
"sha256:07ec6d7d929ae9c68f716195ce15e745b3e8fa122fc67698ac6498d802ed0fa4",
"sha256:0880e239827b4b5b3e2ce05e6b766a7414e5f5aedc4523be6b68cfbc7f61c5d0",
"sha256:0c27f38dc4fbf07b358b2bc90edf35e82d1703e22ff2efa4af4ad5de1b3833e7",
"sha256:0d8a8adef23d86d8eceed3e32e9cca8879c7481c183f84ed1a8edc7df073af94",
"sha256:0e2a35baa428181cb2270a15864ec6286822d3576f2ed0f4cd7f0c1708472aff",
"sha256:0f8682dbdd2f67f8e1edddcbffcc29f60a6182b4901c367fc8c1c40d30bb0a82",
"sha256:0fa467fd300a6f046bdb248d40cd015b21b7576c168a6bb20aa22e595c8ffcdd",
"sha256:128552af70a64660f21cb0eb4876cbdadf1a1f9d5de820fed6421fa8de07c893",
"sha256:1396e81b83516b9d5c9e26a924fa69164156c148c717131f54f586485ac3c15e",
"sha256:149b8a07712f45b332faee1a2258d8ef1fb4a36f88c0c17cb687f205c5dc6e7d",
"sha256:14ac492c686defc8e6133e3a2d9eaf5261b3df26b8ae97450c1647286750b901",
"sha256:14cfbb00959259e15d684505263d5a21732b31248a5dd4941f73a3be233865b9",
"sha256:14e09ff0b8fe6e46b93d36a878f6e4a3a98ba5303c76bb8e716f4878a3bee92c",
"sha256:154ea7c52e32dce13065dbb20a4a6f0cc012b4f667ac90d648d36b12007fa9f7",
"sha256:15d6bca84ffc966cc9976b09a18cf9543ed4d4ecbd97e7086f9ce9327ea48891",
"sha256:1d40f55222b233e98e3921df7811c27567f0e1a4411b93d4c5c0f4ce131bc42f",
"sha256:25bd966103890ccfa028841a8f30cebcf5875eeac8c4bde4fe221364c92f0c9a",
"sha256:2cf5bb4dd67f20f3bbc1209ef572a259027c49e5ff694fa56bed62959b41e1f9",
"sha256:2e0e2959ef5d5b8dc9ef21e1a305a21a36e254e6a34432d00c72a92fdc5ecda5",
"sha256:320f14bd4542a04ab23747ff2c8a778bde727158b606e2661349557f0770711e",
"sha256:3625578b6010c65964d177626fde80cf60d7f2e297d56b925cb5cdeda6e9925a",
"sha256:39215d809470f4c8d1881758575b2abfb80174a9e8daf8f33b1d4379357e417c",
"sha256:3f0ac9fb8608dbc6eaf17956bf623c9119b4db7dbb511650910a82e261e6600f",
"sha256:417243bf599ba1f1fef2bb8c543ceb918676954734e2dcb82bf162ae9d7bd514",
"sha256:420a692b547736a8d8703c39ea935ab5d8f0d2573f8f123b0a294e49a73f214b",
"sha256:443fed67d33aa85357464f297e3d26e570267d1af6fef1c21ca50921d2976302",
"sha256:48525933fea744a3e7464c19bfede85df4aba79ce90c60b94d8b6e1eddd67096",
"sha256:485a91abe3a07c3a8d1e082ba29254eea3e2bb13cbbd4351ea4e5a21912cc9b0",
"sha256:4a5be350f922430997f240d25f8219f93b0c81e15f7b30b868b2fddfc2d05f27",
"sha256:4d966c47f9dd73c2d32a809d2be529112d509321c5310ebf54076812e6ecd884",
"sha256:524ff0ca3baea164d6d93a32c58ac79eca9f6cf713586fdc0adb66a8cdeab96a",
"sha256:53df009d1e1ba40f696f8995683e067e3967101d4bb4ea6f667931b7d4a01357",
"sha256:5994985da903d0b8a08e4935c46ed8daf5be1cf217489e673910951dc533d430",
"sha256:5cabb9710f09d5d2e9e2748c3e3e20d991a4c5f96ed8f1132518f54ab2967221",
"sha256:5fdb39f67c779b183b0c853cd6b45f7db84b84e0571b3ef1c89cdb1dfc367325",
"sha256:600d04a7b342363058b9190d4e929a8e2e715c5682a70cc37d5ded1e0dd370b4",
"sha256:631cb7415225954fdcc2a024119101946793e5923f6c4d73a5914d27eb3d3a05",
"sha256:63974d168b6233b4ed6a0046296803cb13c56637a7b8106564ab575926572a55",
"sha256:64322bfa13e44c6c30c518729ef08fda6026b96d5c0be724b3c4ae4da939f875",
"sha256:655f8f4c8d6a5963c9a0687793da37b9b681d9ad06f29438a3b2326d4e6b7970",
"sha256:6835451b57c1b467b95ffb03a38bb75b52fb4dc2762bb1d9dbed8de31ea7d0fc",
"sha256:6db2eb9654a85ada248afa5a6db5ff1cf0f7b16043a6b070adc4a5be68c716d6",
"sha256:7c4d1894fe112b0864c1fa75dffa045720a194b227bed12f4be7f6045b25209f",
"sha256:7eb037106f5c6b3b0b864ad226b0b7ab58157124161d48e4b30c4a43fef8bc4b",
"sha256:8282bab177a9a3081fd3d0a0175a07a1e2bfb7fcbbd949519ea0980f8a07144d",
"sha256:82f55187a5bebae7d81d35b1e9aaea5e169d44819789837cdd4720d768c55d15",
"sha256:8572cadbf4cfa95fb4187775b5ade2eaa93511f07947b38f4cd67cf10783b118",
"sha256:8cdbbd92154db2fec4ec973d45c565e767ddc20aa6dbaf50142676484cbff8ee",
"sha256:8f6e6aed5818c264412ac0598b581a002a9f050cb2637a84979859e70197aa9e",
"sha256:92f675fefa977625105708492850bcbc1182bfc3e997f8eecb866d1927c98ae6",
"sha256:962ed72424bf1f72334e2f1e61b68f16c0e596f024ca7ac5daf229f7c26e4208",
"sha256:9badf8d45171d92387410b04639d73811b785b5161ecadabf056ea14d62d4ede",
"sha256:9c120c9ce3b163b985a3b966bb701114beb1da4b0468b9b236fc754783d85aa3",
"sha256:9f6f3e2598604956480f6c8aa24a3384dbf6509fe995d97f6ca6103bb8c2534e",
"sha256:a1254357f7e4c82e77c348dabf2d55f1d14d19d91ff025004775e70a6ef40ada",
"sha256:a1392e0638af203cee360495fd2cfdd6054711f2db5175b6e9c3c461b76f5175",
"sha256:a1c311fd06ab3b10805abb72109f01a134019739bd3286b8ae1bc2fc4e50c07a",
"sha256:a5cb87bdc2e5f620693148b5f8f842d293cae46c5f15a1b1bf7ceeed324a740c",
"sha256:a7a7902bf75779bc12ccfc508bfb7a4c47063f748ea3de87135d433a4cca7a2f",
"sha256:aad7bd686363d1ce4ee930ad39f14e1673248373f4a9d74d2b9554f06199fb58",
"sha256:aafdb89fdeb5fe165043896817eccd6434aee124d5ee9b354f92cd574ba5e78f",
"sha256:ae8a8843b11dc0b03b57b52793e391f0122e740de3df1474814c700d2622950a",
"sha256:b00bc4619f60c853556b35f83731bd817f989cba3e97dc792bb8c97941b8053a",
"sha256:b1f22a9ab44de5f082216270552aa54259db20189e68fc12484873d926426921",
"sha256:b3c01c2fb081fced3bbb3da78510693dc7121bb893a1f0f5f4b48013201f362e",
"sha256:b3dcd587b69bbf54fc04ca157c2323b8911033e827fffaecf0cafa5a892a0904",
"sha256:b4a6db486ac8e99ae696e09efc8b2b9fea67b63c8f88ba7a1a16c24a057a0776",
"sha256:bec7dd208a4182e99c5b6c501ce0b1f49de2802448d4056091f8e630b28e9a52",
"sha256:c0877239307b7e69d025b73774e88e86ce82f6ba6adf98f41069d5b0b78bd1bf",
"sha256:caa48fc31fc7243e50188197b5f0c4228956f97b954f76da157aae7f67269ae8",
"sha256:cfe1090245c078720d250d19cb05d67e21a9cd7c257698ef139bc41cf6c27b4f",
"sha256:d43002441932f9a9ea5d6f9efaa2e21458221a3a4b417a14027a1d530201ef1b",
"sha256:d64728ee14e667ba27c66314b7d880b8eeb050e58ffc5fec3b7a109f8cddbd63",
"sha256:d6495008733c7521a89422d7a68efa0a0122c99a5861f06020ef5b1f51f9ba7c",
"sha256:d8f1ebca515a03e5654f88411420fea6380fc841d1bea08effb28184e3d4899f",
"sha256:d99277877daf2efe074eae6338453a4ed54a2d93fb4678ddfe1209a0c93a2468",
"sha256:da01bec0a26befab4898ed83b362993c844b9a607a86add78604186297eb047e",
"sha256:db9a28c063c7c00844ae42a80203eb6d2d6bbb97070cfa00194dff40e6f545ab",
"sha256:dda81e5ec82485155a19d9624cfcca9be88a405e2857354e5b089c2a982144b2",
"sha256:e357571bb0efd65fd55f18db0a2fb0ed89d0bb1d41d906b138f088933ae618bb",
"sha256:e544246b859f17373bed915182ab841b80849ed9cf23f1f07b73b7c58baee5fb",
"sha256:e562617a45b5a9da5be4abe72b971d4f00bf8555eb29bb91ec2ef2be348cd132",
"sha256:e570ffeb2170e116a5b17e83f19911020ac79d19c96f320cbfa1fa96b470185b",
"sha256:e6f31a17acede6a8cd1ae2d123ce04d8cca74056c9d456075f4f6f85de055607",
"sha256:e9121b4009339b0f751955baf4543a0bfd6bc3f8188f8056b1a25a2d45099934",
"sha256:ebedb45b9feb7258fac0a268a3f6bec0a2ea4d9558f3d6f813f02ff3a6dc6698",
"sha256:ecaac27da855b8d73f92123e5f03612b04c5632fd0a476e469dfc47cd37d6b2e",
"sha256:ecdbde46235f3d560b18be0cb706c8e8ad1b965e5c13bbba7450c86064e96561",
"sha256:ed550ed05540c03f0e69e6d74ad58d026de61b9eaebebbaaf8873e585cbb18de",
"sha256:eeb3d3d6b399ffe55f9a04e09e635554012f1980696d6b0aca3e6cf42a17a03b",
"sha256:ef337945bbd76cce390d1b2496ccf9f90b1c1242a3a7bc242ca4a9fc5993427a",
"sha256:f1365e032a477c1430cfe0cf2856679529a2331426f8081172c4a74186f1d595",
"sha256:f23b55eb5464468f9e0e9a9935ce3ed2a870608d5f534025cd5536bca25b1402",
"sha256:f2e9072d71c1f6cfc79a36d4484c82823c560e6f5599c43c1ca6b5cdbd54f881",
"sha256:f323306d0556351735b54acbf82904fe30a27b6a7147153cbe6e19aaaa2aa429",
"sha256:f36a3489d9e28fe4b67be9992a23029c3cec0babc3bd9afb39f49844a8c721c5",
"sha256:f64f82cc3443149292b32387086d02a6c7fb39b8781563e0ca7b8d7d9cf72bd7",
"sha256:f6defd966ca3b187ec6c366604e9296f585021d922e666b99c47e78738b5666c",
"sha256:f7c2b8eb9fc872e68b46eeaf835e86bccc3a58ba57d0eedc109cbb14177be531",
"sha256:fa7db7558607afeccb33c0e4bf1c9a9a835e26599e76af6fe2fcea45904083a6",
"sha256:fcb83175cc4936a5425dde3356f079ae03c0802bbdf8ff82c035f8a54b333521"
],
"markers": "python_version >= '3.7'",
"version": "==2.10.1"
},
"pytest": {
"hashes": [
"sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002",
"sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==7.4.2"
},
"pytest-html": {
"hashes": [
"sha256:88682b9e8e51392472546a70a2139b27d6bc1834a4afd3e41da33c9d9f91e4a4",
"sha256:907c3e68462df129d3ee96dee58bd63f70216b06421836b22fd3fd57ef314acb"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==4.0.2"
},
"pytest-metadata": {
"hashes": [
"sha256:769a9c65d2884bd583bc626b0ace77ad15dbe02dd91a9106d47fd46d9c2569ca",
"sha256:a17b1e40080401dc23177599208c52228df463db191c1a573ccdffacd885e190"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.0.0"
},
"pytest-variables": {
"hashes": [
"sha256:190d9d4da5a6013eb02df2049f6047d911cdbe44c5b1734a6acc1748433c93d0",
"sha256:ab84235417afac5a0a7dd4c3918287d9c7329d2e16d570d6e943f8d8e02533b9"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==3.0.0"
},
"pyyaml": {
"hashes": [
"sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5",
"sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc",
"sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df",
"sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741",
"sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206",
"sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27",
"sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595",
"sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62",
"sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98",
"sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696",
"sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290",
"sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9",
"sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d",
"sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6",
"sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867",
"sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47",
"sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486",
"sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6",
"sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3",
"sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007",
"sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938",
"sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0",
"sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c",
"sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735",
"sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d",
"sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28",
"sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4",
"sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba",
"sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8",
"sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5",
"sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd",
"sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3",
"sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0",
"sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515",
"sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c",
"sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c",
"sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924",
"sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34",
"sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43",
"sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859",
"sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673",
"sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54",
"sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a",
"sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b",
"sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab",
"sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa",
"sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c",
"sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585",
"sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d",
"sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"
],
"index": "pypi",
"markers": "python_version >= '3.6'",
"version": "==6.0.1"
},
"requests": {
"hashes": [
"sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f",
"sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"
],
"index": "pypi",
"markers": "python_version >= '3.7'",
"version": "==2.31.0"
},
"typing-extensions": {
"hashes": [
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
],
"markers": "python_version >= '3.8'",
"version": "==4.8.0"
},
"urllib3": {
"hashes": [
"sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84",
"sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.7"
}
},
"develop": {
"black": {
"hashes": [
"sha256:0e232f24a337fed7a82c1185ae46c56c4a6167fb0fe37411b43e876892c76699",
"sha256:30b78ac9b54cf87bcb9910ee3d499d2bc893afd52495066c49d9ee6b21eee06e",
"sha256:31946ec6f9c54ed7ba431c38bc81d758970dd734b96b8e8c2b17a367d7908171",
"sha256:31b9f87b277a68d0e99d2905edae08807c007973eaa609da5f0c62def6b7c0bd",
"sha256:47c4510f70ec2e8f9135ba490811c071419c115e46f143e4dce2ac45afdcf4c9",
"sha256:481167c60cd3e6b1cb8ef2aac0f76165843a374346aeeaa9d86765fe0dd0318b",
"sha256:6901631b937acbee93c75537e74f69463adaf34379a04eef32425b88aca88a23",
"sha256:76baba9281e5e5b230c9b7f83a96daf67a95e919c2dfc240d9e6295eab7b9204",
"sha256:7fb5fc36bb65160df21498d5a3dd330af8b6401be3f25af60c6ebfe23753f747",
"sha256:960c21555be135c4b37b7018d63d6248bdae8514e5c55b71e994ad37407f45b8",
"sha256:a3c2ddb35f71976a4cfeca558848c2f2f89abc86b06e8dd89b5a65c1e6c0f22a",
"sha256:c870bee76ad5f7a5ea7bd01dc646028d05568d33b0b09b7ecfc8ec0da3f3f39c",
"sha256:d3d9129ce05b0829730323bdcb00f928a448a124af5acf90aa94d9aba6969604",
"sha256:db451a3363b1e765c172c3fd86213a4ce63fb8524c938ebd82919bf2a6e28c6a",
"sha256:e223b731a0e025f8ef427dd79d8cd69c167da807f5710add30cdf131f13dd62e",
"sha256:f20ff03f3fdd2fd4460b4f631663813e57dc277e37fb216463f3b907aa5a9bdd",
"sha256:f74892b4b836e5162aa0452393112a574dac85e13902c57dfbaaf388e4eda37c",
"sha256:f8dc7d50d94063cdfd13c82368afd8588bac4ce360e4224ac399e769d6704e98"
],
"index": "pypi",
"markers": "python_version >= '3.8'",
"version": "==23.10.0"
},
"click": {
"hashes": [
"sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28",
"sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.7"
},
"flake8": {
"hashes": [
"sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23",
"sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"
],
"index": "pypi",
"markers": "python_full_version >= '3.8.1'",
"version": "==6.1.0"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy-extensions": {
"hashes": [
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
],
"markers": "python_version >= '3.5'",
"version": "==1.0.0"
},
"packaging": {
"hashes": [
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
],
"markers": "python_version >= '3.7'",
"version": "==23.2"
},
"pathspec": {
"hashes": [
"sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20",
"sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"
],
"markers": "python_version >= '3.7'",
"version": "==0.11.2"
},
"platformdirs": {
"hashes": [
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
],
"markers": "python_version >= '3.7'",
"version": "==3.11.0"
},
"pycodestyle": {
"hashes": [
"sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f",
"sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"
],
"markers": "python_version >= '3.8'",
"version": "==2.11.1"
},
"pyflakes": {
"hashes": [
"sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774",
"sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"
],
"markers": "python_version >= '3.8'",
"version": "==3.1.0"
}
}
}

@ -0,0 +1,96 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.experimentintegration
import android.content.pm.ActivityInfo
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen
/**
* Tests for verifying functionality of the message survey surface
*/
class SurveyExperimentIntegrationTest {
private val surveyURL = "qsurvey.mozilla.com"
private val experimentName = "Viewpoint"
@get:Rule
val activityTestRule = HomeActivityTestRule(
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
isDeleteSitePermissionsEnabled = true,
)
@Before
fun setUp() {
TestHelper.appContext.settings().showSecretDebugMenuThisSession = true
}
@After
fun tearDown() {
TestHelper.appContext.settings().showSecretDebugMenuThisSession = false
}
fun checkExperimentExists() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openExperimentsMenu {
verifyExperimentExists(experimentName)
}
}
@Test
fun checkSurveyNavigatesCorrectly() {
browserScreen {
verifySurveyButton()
}.clickSurveyButton {
verifyUrl(surveyURL)
}
checkExperimentExists()
}
@Test
fun checkSurveyNoThanksNavigatesCorrectly() {
browserScreen {
verifySurveyNoThanksButton()
}.clickNoThanksSurveyButton {
verifyTabCounter("0")
}
checkExperimentExists()
}
@Test
fun checkHomescreenSurveyDismissesCorrectly() {
browserScreen {
verifyHomeScreenSurveyCloseButton()
}.clickHomeScreenSurveyCloseButton {
verifyTabCounter("0")
verifySurveyButtonDoesNotExist()
}
checkExperimentExists()
}
@Test
fun checkSurveyLandscapeLooksCorrect() {
activityTestRule.activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
browserScreen {
verifySurveyNoThanksButton()
verifySurveyButton()
}
checkExperimentExists()
}
}

@ -0,0 +1,226 @@
# 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/.
import json
import logging
import os
from pathlib import Path
import subprocess
import time
import pytest
import requests
from experimentintegration.gradlewbuild import GradlewBuild
from experimentintegration.models.models import TelemetryModel
KLAATU_SERVER_URL = "http://localhost:1378"
KLAATU_LOCAL_SERVER_URL = "http://localhost:1378"
here = Path()
def pytest_addoption(parser):
parser.addoption(
"--experiment", action="store", help="The experiments experimenter URL"
)
parser.addoption(
"--stage", action="store_true", default=None, help="Use the stage server"
)
@pytest.fixture(name="load_branches")
def fixture_load_branches(experiment_url):
branches = []
if experiment_url:
data = experiment_url
else:
try:
data = requests.get(f"{KLAATU_SERVER_URL}/experiment").json()
except ConnectionRefusedError:
logging.warn("No URL or experiment slug provided, exiting.")
exit()
else:
for item in reversed(data):
data = item
break
experiment = requests.get(data).json()
for item in experiment["branches"]:
branches.append(item["slug"])
return branches
@pytest.fixture
def gradlewbuild_log(pytestconfig, tmpdir):
gradlewbuild_log = f"{tmpdir.join('gradlewbuild.log')}"
pytestconfig._gradlewbuild_log = gradlewbuild_log
yield gradlewbuild_log
@pytest.fixture
def gradlewbuild(gradlewbuild_log):
yield GradlewBuild(gradlewbuild_log)
@pytest.fixture(name="experiment_data")
def fixture_experiment_data(experiment_url):
data = requests.get(experiment_url).json()
branches = next(iter(data.get("branches")), None)
features = next(iter(branches.get("features")), None)
if features.get("messages"):
for item in features["value"]["messages"].values():
item["surface"] = "homescreen"
item["style"] = "URGENT"
for count, trigger in enumerate(item["trigger"]):
if "USER_EN_SPEAKER" not in trigger:
del item["trigger"][count]
return [data]
@pytest.fixture(name="experiment_url", scope="module")
def fixture_experiment_url(request, variables):
url = None
if slug := request.config.getoption("--experiment"):
# Build URL from slug
if request.config.getoption("--stage"):
url = f"{variables['urls']['stage_server']}/api/v6/experiments/{slug}"
else:
url = f"{variables['urls']['prod_server']}/api/v6/experiments/{slug}"
else:
try:
data = requests.get(f"{KLAATU_SERVER_URL}/experiment").json()
except requests.exceptions.ConnectionError:
logging.error("No URL or experiment slug provided, exiting.")
exit()
else:
for item in data:
if isinstance(item, dict):
continue
else:
url = item
yield url
return_data = {"url": url}
try:
requests.put(f"{KLAATU_SERVER_URL}/experiment", json=return_data)
except requests.exceptions.ConnectionError:
pass
@pytest.fixture(name="json_data")
def fixture_json_data(tmp_path, experiment_data):
path = tmp_path / "data"
path.mkdir()
json_path = path / "data.json"
with open(json_path, "w", encoding="utf-8") as f:
# URL of experiment/klaatu server
data = {"data": experiment_data}
json.dump(data, f)
return json_path
@pytest.fixture(name="experiment_slug")
def fixture_experiment_slug(experiment_data):
return experiment_data[0]["slug"]
@pytest.fixture(name="start_app")
def fixture_start_app():
def _():
command = f"nimbus-cli --app fenix --channel developer open"
try:
out = subprocess.check_output(
command,
cwd=os.path.join(here, os.pardir),
stderr=subprocess.STDOUT,
universal_newlines=True,
shell=True,
)
except subprocess.CalledProcessError as e:
out = e.output
raise
finally:
with open(gradlewbuild_log, "w") as f:
f.write(out)
time.sleep(
15
) # Wait a while as there's no real way to know when the app has started
return _
@pytest.fixture(name="send_test_results", autouse=True)
def fixture_send_test_results():
yield
here = Path()
with open(f"{here.resolve()}/results/index.html", "rb") as f:
files = {"file": f}
try:
requests.post(f"{KLAATU_SERVER_URL}/test_results", files=files)
except requests.exceptions.ConnectionError:
pass
@pytest.fixture(name="check_ping_for_experiment")
def fixture_check_ping_for_experiment(experiment_slug, variables):
def _check_ping_for_experiment(
branch=None, experiment=experiment_slug, reason=None
):
model = TelemetryModel(branch=branch, experiment=experiment)
timeout = time.time() + 60 * 5
while time.time() < timeout:
data = requests.get(f"{variables['urls']['telemetry_server']}/pings").json()
events = []
for item in data:
event_items = item.get("events")
if event_items:
for event in event_items:
if (
"category" in event
and "nimbus_events" in event["category"]
and "extra" in event
and "branch" in event["extra"]
):
events.append(event)
for event in events:
event_name = event.get("name")
if (reason == "enrollment" and event_name == "enrollment") or (
reason == "unenrollment"
and event_name in ["unenrollment", "disqualification"]
):
telemetry_model = TelemetryModel(
branch=event["extra"]["branch"],
experiment=event["extra"]["experiment"],
)
if model == telemetry_model:
return True
time.sleep(5)
return False
return _check_ping_for_experiment
@pytest.fixture(name="setup_experiment")
def fixture_setup_experiment(experiment_slug, json_data, gradlewbuild_log, variables):
def _(branch):
requests.delete(f"{variables['urls']['telemetry_server']}/pings")
logging.info(f"Testing experiment {experiment_slug}, BRANCH: {branch[0]}")
command = f"nimbus-cli --app fenix --channel developer enroll {experiment_slug} --branch {branch[0]} --file {json_data} --reset-app"
logging.info(f"Running command {command}")
try:
out = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
out = e.output
raise
finally:
with open(gradlewbuild_log, "w") as f:
f.write(f"{out}")
time.sleep(
15
) # Wait a while as there's no real way to know when the app has started
return _

@ -0,0 +1,82 @@
import subprocess
from pathlib import Path
import yaml
def search_for_smoke_tests(tests_name):
"""Searches for smoke tests within the requested test module."""
path = Path("../ui")
files = sorted([x for x in path.iterdir() if x.is_file()])
locations = []
file_name = None
test_names = []
for name in files:
if tests_name in name.name:
file_name = name
break
with open(file_name, "r") as file:
code = file.read().split(" ")
code = [item for item in code if item != ""]
for count, item in enumerate(code):
if "class" in item or "@SmokeTest" in item:
locations.append(count)
for location in locations:
if len(test_names) == 0:
class_name = code[location + 1]
test_names.append(class_name)
else:
test_names.append(f"{class_name}#{code[location+3].strip('()')}")
return test_names
def create_test_file():
"""Create the python file to hold the tests."""
path = Path("tests/")
filename = "test_smoke_scenarios.py"
final_path = path / filename
if final_path.exists():
print("File Exists, you need to delete it to create a new one.")
return
# file exists
subprocess.run([f"touch {final_path}"], encoding="utf8", shell=True)
assert final_path.exists()
with open(final_path, "w") as file:
file.write("import pytest\n\n")
def generate_smoke_tests(tests_names=None):
"""Generate pytest code for the requested tests."""
pytest_file = "tests/test_smoke_scenarios.py"
tests = []
for test in tests_names[1:]:
test_name = test.replace("#", "_").lower()
tests.append(
f"""
@pytest.mark.smoke_test
def test_smoke_{test_name}(setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment):
setup_experiment(load_branches)
gradlewbuild.test("{test}", smoke=True)
assert check_ping_for_experiment
"""
)
with open(pytest_file, "a") as file:
for item in tests:
file.writelines(f"{item}")
if __name__ == "__main__":
test_modules = None
create_test_file()
with open("variables.yaml", "r") as file:
test_modules = yaml.safe_load(file)
for item in test_modules.get("smoke_tests"):
tests = search_for_smoke_tests(item)
generate_smoke_tests(tests)

@ -0,0 +1,50 @@
import logging
import os
import subprocess
from syncintegration.adbrun import ADBrun
here = os.path.dirname(__file__)
logging.getLogger(__name__).addHandler(logging.NullHandler())
class GradlewBuild(object):
binary = "./gradlew"
logger = logging.getLogger()
adbrun = ADBrun()
def __init__(self, log):
self.log = log
def test(self, identifier, smoke=None):
# self.adbrun.launch()
# Change path accordingly to go to root folder to run gradlew
os.chdir("../../../../../../../..")
test_type = "ui" if smoke else "experimentintegration"
cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.{test_type}.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
# if smoke:
# cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.ui.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
# else:
# cmd = f"adb shell am instrument -w -e class org.mozilla.fenix.experimentintegration.{identifier} org.mozilla.fenix.debug.test/androidx.test.runner.AndroidJUnitRunner"
self.logger.info("Running cmd: {}".format(cmd))
out = ""
try:
out = subprocess.check_output(
cmd, encoding="utf8", shell=True, stderr=subprocess.STDOUT
)
if "FAILURES" in out:
raise (AssertionError(out))
except subprocess.CalledProcessError as e:
out = e.output
raise
finally:
# Set the path correctly
tests_path = (
"app/src/androidTest/java/org/mozilla/fenix/experimentintegration/"
)
os.chdir(tests_path)
with open(self.log, "w") as f:
f.write(str(out))

@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -e
echo "Waiting emulator is ready..."
~/Library/Android/sdk/emulator/emulator -avd Pixel_3_API_28 -wipe-data -no-boot-anim -screen no-touch &
bootanim=""
failcounter=0
timeout_in_sec=360
until [[ "$bootanim" =~ "stopped" ]]; do
bootanim=`~/Library/Android/sdk/platform-tools/adb -e shell getprop init.svc.bootanim 2>&1 &`
if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
|| "$bootanim" =~ "running" ]]; then
let "failcounter += 1"
echo "Waiting for emulator to start"
if [[ $failcounter -gt timeout_in_sec ]]; then
echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
exit 1
fi
fi
sleep 1
done
echo "Emulator is ready"
sleep 10

@ -0,0 +1,3 @@
# 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/.

@ -0,0 +1,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
"""Data class Models"""
from pydantic import BaseModel
class TelemetryModel(BaseModel):
"""Experiment Telemetry model"""
branch: str
experiment: str

@ -0,0 +1,4 @@
[pytest]
addopts = --verbose --html=results/index.html --self-contained-html --variables=variables.yaml
log_cli = true
log_cli_level = info

@ -0,0 +1,25 @@
import pytest
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_experiment_unenrolls_via_studies_toggle(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test("GenericExperimentIntegrationTest#disableStudiesViaStudiesToggle")
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_experiment_unenrolls_via_secret_menu(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test(
"GenericExperimentIntegrationTest#testExperimentUnenrolledViaSecretMenu"
)
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
gradlewbuild.test("GenericExperimentIntegrationTest#testExperimentUnenrolled")
assert check_ping_for_experiment(reason="unenrollment", branch=load_branches[0])

@ -0,0 +1,47 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import pytest
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_survey_navigates_correctly(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test("SurveyExperimentIntegrationTest#checkSurveyNavigatesCorrectly")
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_survey_no_thanks_navigates_correctly(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test(
"SurveyExperimentIntegrationTest#checkSurveyNoThanksNavigatesCorrectly"
)
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_homescreen_survey_dismisses_correctly(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test(
"SurveyExperimentIntegrationTest#checkHomescreenSurveyDismissesCorrectly"
)
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])
@pytest.mark.parametrize("load_branches", [("branch")], indirect=True)
def test_survey_landscape_looks_correct(
setup_experiment, gradlewbuild, load_branches, check_ping_for_experiment
):
setup_experiment(load_branches)
gradlewbuild.test(
"SurveyExperimentIntegrationTest#checkSurveyLandscapeLooksCorrect"
)
assert check_ping_for_experiment(reason="enrollment", branch=load_branches[0])

@ -0,0 +1,10 @@
# 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/.
urls:
stage_server: "https://stage.experimenter.nonprod.dataops.mozgcp.net"
prod_server: "https://experimenter.services.mozilla.com"
telemetry_server: "http://172.25.58.187:5000"
smoke_tests:
- "AddressAutofillTest"

@ -0,0 +1,34 @@
/* 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.extensions
import android.content.Context
import mozilla.components.concept.engine.EngineSession
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.gecko.GeckoProvider
import org.mozilla.fenix.helpers.TestHelper
/**
* Instrumentation test for verifying that the extensions process is enabled unconditionally.
*/
class ExtensionProcessTest {
private lateinit var context: Context
private lateinit var policy: EngineSession.TrackingProtectionPolicy
@Before
fun setUp() {
context = TestHelper.appContext
policy = context.components.core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
}
@Test
fun test_extension_process_is_enabled() {
val runtime = GeckoProvider.createRuntimeSettings(context, policy)
assertTrue(runtime.extensionsProcessEnabled!!)
}
}

@ -0,0 +1,427 @@
/* 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/. */
@file:Suppress("DEPRECATION")
package org.mozilla.fenix.helpers
import android.Manifest
import android.app.ActivityManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.storage.StorageManager
import android.os.storage.StorageVolume
import android.provider.Settings
import android.util.Log
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.Espresso
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.intent.Intents.intended
import androidx.test.espresso.intent.matcher.IntentMatchers.toPackage
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule
import androidx.test.runner.permission.PermissionRequester
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import junit.framework.AssertionFailedError
import kotlinx.coroutines.runBlocking
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.customtabs.ExternalAppBrowserActivity
import org.mozilla.fenix.helpers.Constants.PackageName.PIXEL_LAUNCHER
import org.mozilla.fenix.helpers.Constants.PackageName.YOUTUBE_APP
import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.NetworkConnectionIdlingResource
import org.mozilla.fenix.ui.robots.BrowserRobot
import org.mozilla.gecko.util.ThreadUtils
import java.io.File
import java.util.Locale
object AppAndSystemHelper {
fun getPermissionAllowID(): String {
return when
(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
true -> "com.android.permissioncontroller"
false -> "com.android.packageinstaller"
}
}
/**
* Checks if a specific download file is inside the device storage and deletes it.
* Different implementation needed for newer API levels,
* as Environment.getExternalStorageDirectory() is deprecated starting with API 29.
*
*/
fun deleteDownloadedFileOnStorage(fileName: String) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
val storageManager: StorageManager? =
TestHelper.appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
val storageVolumes = storageManager!!.storageVolumes
val storageVolume: StorageVolume = storageVolumes[0]
val file = File(storageVolume.directory!!.path + "/Download/" + fileName)
try {
if (file.exists()) {
file.delete()
Log.d("TestLog", "File delete try 1")
Assert.assertFalse("The file was not deleted", file.exists())
}
} catch (e: AssertionError) {
file.delete()
Log.d("TestLog", "File delete retried")
Assert.assertFalse("The file was not deleted", file.exists())
}
} else {
runBlocking {
val downloadedFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName,
)
if (downloadedFile.exists()) {
Log.i(TAG, "deleteDownloadedFileOnStorage: Verifying if $downloadedFile exists.")
downloadedFile.delete()
Log.i(TAG, "deleteDownloadedFileOnStorage: $downloadedFile deleted.")
}
}
}
}
/**
* Checks if there are download files inside the device storage and deletes all of them.
* Different implementation needed for newer API levels, as
* Environment.getExternalStorageDirectory() is deprecated starting with API 29.
*/
fun clearDownloadsFolder() {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
Log.i(TAG, "clearDownloadsFolder: API > 29")
val storageManager: StorageManager? =
TestHelper.appContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager?
val storageVolumes = storageManager!!.storageVolumes
val storageVolume: StorageVolume = storageVolumes[0]
val downloadsFolder = File(storageVolume.directory!!.path + "/Download/")
// Check if the downloads folder exists
if (downloadsFolder.exists() && downloadsFolder.isDirectory) {
Log.i(TAG, "clearDownloadsFolder: Verified that \"DOWNLOADS\" folder exists")
val files = downloadsFolder.listFiles()
// Check if the folder is not empty
if (files != null && files.isNotEmpty()) {
Log.i(
TAG,
"clearDownloadsFolder: Verified that \"DOWNLOADS\" folder is not empty",
)
// Delete all files in the folder
for (file in files) {
file.delete()
Log.i(TAG, "clearDownloadsFolder: Deleted $file from \"DOWNLOADS\" folder")
}
}
}
} else {
runBlocking {
Log.i(TAG, "clearDownloadsFolder: API <= 29")
Log.i(TAG, "clearDownloadsFolder: Verifying if any download files exist.")
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.listFiles()?.forEach {
it.delete()
Log.i(TAG, "clearDownloadsFolder: Download file $it deleted.")
}
}
}
}
fun setNetworkEnabled(enabled: Boolean) {
val networkDisconnectedIdlingResource = NetworkConnectionIdlingResource(false)
val networkConnectedIdlingResource = NetworkConnectionIdlingResource(true)
when (enabled) {
true -> {
mDevice.executeShellCommand("svc data enable")
mDevice.executeShellCommand("svc wifi enable")
// Wait for network connection to be completely enabled
IdlingRegistry.getInstance().register(networkConnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkConnectedIdlingResource)
}
Log.i(TAG, "setNetworkEnabled: Network connection was enabled")
}
false -> {
mDevice.executeShellCommand("svc data disable")
mDevice.executeShellCommand("svc wifi disable")
// Wait for network connection to be completely disabled
IdlingRegistry.getInstance().register(networkDisconnectedIdlingResource)
Espresso.onIdle {
IdlingRegistry.getInstance().unregister(networkDisconnectedIdlingResource)
}
Log.i(TAG, "setNetworkEnabled: Network connection was disabled")
}
}
}
fun isPackageInstalled(packageName: String): Boolean {
Log.i(TAG, "isPackageInstalled: Trying to verify that $packageName is installed")
return try {
val packageManager = InstrumentationRegistry.getInstrumentation().context.packageManager
packageManager.getApplicationInfo(packageName, 0).enabled
} catch (e: PackageManager.NameNotFoundException) {
Log.i(TAG, "isPackageInstalled: $packageName is not installed - ${e.message}")
false
}
}
fun assertExternalAppOpens(appPackageName: String) {
if (isPackageInstalled(appPackageName)) {
Log.i(TAG, "assertExternalAppOpens: $appPackageName is installed on device")
try {
Log.i(TAG, "assertExternalAppOpens: Try block")
intended(toPackage(appPackageName))
Log.i(TAG, "assertExternalAppOpens: Matched intent to $appPackageName")
} catch (e: AssertionFailedError) {
Log.i(TAG, "assertExternalAppOpens: Catch block - ${e.message}")
}
} else {
mDevice.waitNotNull(
Until.findObject(By.text("Could not open file")),
TestAssetHelper.waitingTime,
)
Log.i(TAG, "assertExternalAppOpens: Verified \"Could not open file\" message")
}
}
fun assertNativeAppOpens(appPackageName: String, url: String = "") {
if (isPackageInstalled(appPackageName)) {
mDevice.waitForIdle(TestAssetHelper.waitingTimeShort)
Assert.assertTrue(
TestHelper.mDevice.findObject(UiSelector().packageName(appPackageName))
.waitForExists(TestAssetHelper.waitingTime),
)
} else {
BrowserRobot().verifyUrl(url)
}
}
fun assertYoutubeAppOpens() = intended(toPackage(YOUTUBE_APP))
/**
* Checks whether the latest activity of the application is used for custom tabs or PWAs.
*
* @return Boolean value that helps us know if the current activity supports custom tabs or PWAs.
*/
fun isExternalAppBrowserActivityInCurrentTask(): Boolean {
Log.i(TAG, "Trying to verify that the latest activity of the application is used for custom tabs or PWAs")
val activityManager = TestHelper.appContext.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
mDevice.waitForIdle(TestAssetHelper.waitingTimeShort)
return activityManager.appTasks[0].taskInfo.topActivity!!.className == ExternalAppBrowserActivity::class.java.name
}
/**
* Run test with automatically registering idling resources and cleanup.
*
* @param idlingResources zero or more [IdlingResource] to be used when running [testBlock].
* @param testBlock test code to execute.
*/
fun registerAndCleanupIdlingResources(
vararg idlingResources: IdlingResource,
testBlock: () -> Unit,
) {
idlingResources.forEach {
IdlingRegistry.getInstance().register(it)
}
try {
testBlock()
} finally {
idlingResources.forEach {
IdlingRegistry.getInstance().unregister(it)
}
}
}
// Permission allow dialogs differ on various Android APIs
fun grantSystemPermission() {
val whileUsingTheAppPermissionButton: UiObject =
mDevice.findObject(UiSelector().textContains("While using the app"))
val allowPermissionButton: UiObject =
mDevice.findObject(
UiSelector()
.textContains("Allow")
.className("android.widget.Button"),
)
if (Build.VERSION.SDK_INT >= 23) {
if (whileUsingTheAppPermissionButton.waitForExists(TestAssetHelper.waitingTimeShort)) {
whileUsingTheAppPermissionButton.click()
} else if (allowPermissionButton.waitForExists(TestAssetHelper.waitingTimeShort)) {
allowPermissionButton.click()
}
}
}
// Permission deny dialogs differ on various Android APIs
fun denyPermission() {
mDevice.findObject(UiSelector().textContains("Deny")).waitForExists(TestAssetHelper.waitingTime)
mDevice.findObject(UiSelector().textContains("Deny")).click()
}
fun isTestLab(): Boolean {
return Settings.System.getString(TestHelper.appContext.contentResolver, "firebase.test.lab").toBoolean()
}
/**
* Changes the default language of the entire device, not just the app.
* Runs on Debug variant as we don't want to adjust Release permission manifests
* Runs the test in its testBlock.
* Cleans up and sets the default locale after it's done.
* As a safety measure, always add the resetSystemLocaleToEnUS() method in the tearDown method of your Class.
*/
fun runWithSystemLocaleChanged(locale: Locale, testRule: ActivityTestRule<HomeActivity>, testBlock: () -> Unit) {
if (Config.channel.isDebug) {
/* Sets permission to change device language */
PermissionRequester().apply {
addPermissions(
Manifest.permission.CHANGE_CONFIGURATION,
)
requestPermissions()
}
val defaultLocale = Locale.getDefault()
try {
setSystemLocale(locale)
testBlock()
ThreadUtils.runOnUiThread { testRule.activity.recreate() }
} catch (e: Exception) {
e.printStackTrace()
} finally {
setSystemLocale(defaultLocale)
}
}
}
/**
* Resets the default language of the entire device back to EN-US.
* In case of a test instrumentation crash, the finally statement in the
* runWithSystemLocaleChanged(locale: Locale) method, will not be reached.
* Add this method inside the tearDown method of your test class, where the above method is used.
* Note: If set inside the ActivityTestRule's afterActivityFinished() method, this also won't work,
* as the methods inside it are not always executed: https://github.com/android/android-test/issues/498
*/
fun resetSystemLocaleToEnUS() {
if (Locale.getDefault() != Locale.US) {
Log.i(TAG, "Resetting system locale to EN US")
setSystemLocale(Locale.US)
}
}
/**
* Changes the default language of the entire device, not just the app.
*/
fun setSystemLocale(locale: Locale) {
val activityManagerNative = Class.forName("android.app.ActivityManagerNative")
val am = activityManagerNative.getMethod("getDefault", *arrayOfNulls(0))
.invoke(activityManagerNative, *arrayOfNulls(0))
val config = InstrumentationRegistry.getInstrumentation().context.resources.configuration
config.javaClass.getDeclaredField("locale")[config] = locale
config.javaClass.getDeclaredField("userSetLocale").setBoolean(config, true)
am.javaClass.getMethod(
"updateConfiguration",
Configuration::class.java,
).invoke(am, config)
}
fun putAppToBackground() {
mDevice.pressRecentApps()
mDevice.findObject(UiSelector().resourceId("${TestHelper.packageName}:id/container")).waitUntilGone(
TestAssetHelper.waitingTime,
)
}
/**
* Brings the app to foregorund by clicking it in the recent apps tray.
* The package name is related to the home screen experience for the Pixel phones produced by Google.
* The recent apps tray on API 30 will always display only 2 apps, even if previously were opened more.
* The index of the most recent opened app will always have index 2, meaning that the previously opened app will have index 1.
*/
fun bringAppToForeground() =
mDevice.findObject(UiSelector().index(2).packageName(PIXEL_LAUNCHER)).clickAndWaitForNewWindow(waitingTimeShort)
fun verifyKeyboardVisibility(isExpectedToBeVisible: Boolean = true) {
mDevice.waitForIdle()
assertEquals(
"Keyboard not shown",
isExpectedToBeVisible,
mDevice
.executeShellCommand("dumpsys input_method | grep mInputShown")
.contains("mInputShown=true"),
)
}
fun openAppFromExternalLink(url: String) {
val context = InstrumentationRegistry.getInstrumentation().getTargetContext()
val intent = Intent().apply {
action = Intent.ACTION_VIEW
data = Uri.parse(url)
`package` = TestHelper.packageName
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
try {
context.startActivity(intent)
} catch (ex: ActivityNotFoundException) {
intent.setPackage(null)
context.startActivity(intent)
}
}
/**
* Wrapper for tests to run only when certain conditions are met.
* For example: this method will avoid accidentally running a test on GV versions where the feature is disabled.
*/
fun runWithCondition(condition: Boolean, testBlock: () -> Unit) {
if (condition) {
testBlock()
}
}
/**
* Wrapper to launch the app using the launcher intent.
*/
fun runWithLauncherIntent(
activityTestRule: AndroidComposeTestRule<HomeActivityIntentTestRule, HomeActivity>,
testBlock: () -> Unit,
) {
val launcherIntent = Intent(Intent.ACTION_MAIN).apply {
addCategory(Intent.CATEGORY_LAUNCHER)
}
activityTestRule.activityRule.withIntent(launcherIntent).launchActivity(launcherIntent)
try {
testBlock()
} catch (e: Exception) {
e.printStackTrace()
}
}
}

@ -4,19 +4,25 @@
package org.mozilla.fenix.helpers
import org.mozilla.fenix.helpers.TestHelper.getSponsoredShortcutTitle
import org.mozilla.fenix.helpers.DataGenerationHelper.getSponsoredShortcutTitle
object Constants {
// Tag used for logging
const val TAG = "MozUITestLog"
// Device or AVD requires a Google Services Android OS installation
object PackageName {
const val GOOGLE_PLAY_SERVICES = "com.android.vending"
const val GOOGLE_APPS_PHOTOS = "com.google.android.apps.photos"
const val GOOGLE_QUICK_SEARCH = "com.google.android.googlequicksearchbox"
const val GOOGLE_DOCS = "com.google.android.apps.docs"
const val YOUTUBE_APP = "com.google.android.youtube"
const val GMAIL_APP = "com.google.android.gm"
const val PHONE_APP = "com.android.dialer"
const val ANDROID_SETTINGS = "com.android.settings"
const val PRINT_SPOOLER = "com.android.printspooler"
const val PIXEL_LAUNCHER = "com.google.android.apps.nexuslauncher"
}
const val SPEECH_RECOGNITION = "android.speech.action.RECOGNIZE_SPEECH"

@ -0,0 +1,137 @@
/* 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
import android.app.PendingIntent
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import androidx.browser.customtabs.CustomTabsIntent
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiSelector
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.availableSearchEngines
import org.junit.Assert
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.utils.IntentUtils
import java.time.LocalDate
import java.time.LocalTime
object DataGenerationHelper {
val appContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
fun createCustomTabIntent(
pageUrl: String,
customMenuItemLabel: String = "",
customActionButtonDescription: String = "",
): Intent {
val appContext = InstrumentationRegistry.getInstrumentation()
.targetContext
.applicationContext
val pendingIntent = PendingIntent.getActivity(appContext, 0, Intent(), IntentUtils.defaultIntentPendingFlags)
val customTabsIntent = CustomTabsIntent.Builder()
.addMenuItem(customMenuItemLabel, pendingIntent)
.setShareState(CustomTabsIntent.SHARE_STATE_ON)
.setActionButton(
createTestBitmap(),
customActionButtonDescription,
pendingIntent,
true,
)
.build()
customTabsIntent.intent.data = Uri.parse(pageUrl)
return customTabsIntent.intent
}
private fun createTestBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawColor(Color.GREEN)
return bitmap
}
fun getStringResource(id: Int, argument: String = TestHelper.appName) = TestHelper.appContext.resources.getString(id, argument)
private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
fun generateRandomString(stringLength: Int) =
(1..stringLength)
.map { kotlin.random.Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
/**
* Creates clipboard data.
*/
fun setTextToClipBoard(context: Context, message: String) {
val clipBoard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clipData = ClipData.newPlainText("label", message)
clipBoard.setPrimaryClip(clipData)
}
/**
* Constructs a date and time placeholder string for sponsored Fx suggest links.
* The format of the datetime is YYYYMMDDHH, where YYYY is the four-digit year,
* MM is the two-digit month, DD is the two-digit day, and HH is the two-digit hour.
* Single-digit months, days, and hours are padded with a leading zero to ensure
* the correct format. For example, a date and time of January 10, 2024, at 3 PM
* would be represented as "2024011015".
*
* @return A string representing the current date and time in the specified format.
*/
fun getSponsoredFxSuggestPlaceHolder(): String {
val currentDate = LocalDate.now()
val currentTime = LocalTime.now()
val currentDay = currentDate.dayOfMonth.toString().padStart(2, '0')
val currentMonth = currentDate.monthValue.toString().padStart(2, '0')
val currentYear = currentDate.year.toString()
val currentHour = currentTime.hour.toString().padStart(2, '0')
return currentYear + currentMonth + currentDay + currentHour
}
/**
* Returns sponsored shortcut title based on the index.
*/
fun getSponsoredShortcutTitle(position: Int): String {
val sponsoredShortcut = mDevice.findObject(
UiSelector()
.resourceId("${TestHelper.packageName}:id/top_site_item")
.index(position - 1),
).getChild(
UiSelector()
.resourceId("${TestHelper.packageName}:id/top_site_title"),
).text
return sponsoredShortcut
}
/**
* The list of Search engines for the "home" region of the user.
* For en-us it will return the 6 engines selected by default: Google, Bing, DuckDuckGo, Amazon, Ebay, Wikipedia.
*/
fun getRegionSearchEnginesList(): List<SearchEngine> {
val searchEnginesList = appContext.components.core.store.state.search.regionSearchEngines
Assert.assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
return searchEnginesList
}
/**
* The list of Search engines available to be added by user choice.
* For en-us it will return the 2 engines: Reddit, Youtube.
*/
fun getAvailableSearchEngines(): List<SearchEngine> {
val searchEnginesList = TestHelper.appContext.components.core.store.state.search.availableSearchEngines
Assert.assertTrue("Search engines list returned nothing", searchEnginesList.isNotEmpty())
return searchEnginesList
}
}

@ -4,7 +4,7 @@
package org.mozilla.fenix.helpers
import org.mozilla.experiments.nimbus.GleanPlumbMessageHelper
import org.mozilla.experiments.nimbus.NimbusMessagingHelperInterface
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestHelper.appContext
@ -12,7 +12,7 @@ object Experimentation {
val experiments =
appContext.components.analytics.experiments
fun withHelper(block: GleanPlumbMessageHelper.() -> Unit) {
fun withHelper(block: NimbusMessagingHelperInterface.() -> Unit) {
val helper = experiments.createMessageHelper()
block(helper)
}

@ -67,6 +67,26 @@ interface FeatureSettingsHelper {
*/
var etpPolicy: ETPPolicy
/**
* Enable or disable open in app banner.
*/
var isOpenInAppBannerEnabled: Boolean
/**
* Enable or disable the Tabs Tray to Compose rewrite.
*/
var tabsTrayRewriteEnabled: Boolean
/**
* Enable or disable the Top Sites to Compose rewrite.
*/
var composeTopSitesEnabled: Boolean
/**
* Enable or disable translations flow.
*/
var isTranslationsEnabled: Boolean
fun applyFlagUpdates()
fun resetAllFeatureFlags()
@ -83,5 +103,4 @@ enum class ETPPolicy {
STANDARD,
STRICT,
CUSTOM,
;
}

@ -17,7 +17,7 @@ import org.mozilla.fenix.utils.Settings
/**
* Helper for querying the status and modifying various features and settings in the application.
*/
class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
class FeatureSettingsHelperDelegate() : FeatureSettingsHelper {
/**
* The current feature flags used inside the app before the tests start.
* These will be restored when the tests end.
@ -33,7 +33,11 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR,
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions,
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner,
etpPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled = settings.enableTabsTrayToCompose,
composeTopSitesEnabled = settings.enableComposeTopSites,
translationsEnabled = settings.enableTranslations,
)
/**
@ -51,6 +55,7 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
false -> 0
}
}
override var isPocketEnabled: Boolean by updatedFeatureFlags::isPocketEnabled
override var isJumpBackInCFREnabled: Boolean by updatedFeatureFlags::isJumpBackInCFREnabled
override var isWallpaperOnboardingEnabled: Boolean by updatedFeatureFlags::isWallpaperOnboardingEnabled
@ -58,7 +63,11 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
override var isRecentlyVisitedFeatureEnabled: Boolean by updatedFeatureFlags::isRecentlyVisitedFeatureEnabled
override var isPWAsPromptEnabled: Boolean by updatedFeatureFlags::isPWAsPromptEnabled
override var isTCPCFREnabled: Boolean by updatedFeatureFlags::isTCPCFREnabled
override var isOpenInAppBannerEnabled: Boolean by updatedFeatureFlags::isOpenInAppBannerEnabled
override var etpPolicy: ETPPolicy by updatedFeatureFlags::etpPolicy
override var tabsTrayRewriteEnabled: Boolean by updatedFeatureFlags::tabsTrayRewriteEnabled
override var composeTopSitesEnabled: Boolean by updatedFeatureFlags::composeTopSitesEnabled
override var isTranslationsEnabled: Boolean by updatedFeatureFlags::translationsEnabled
override fun applyFlagUpdates() {
applyFeatureFlags(updatedFeatureFlags)
@ -67,6 +76,7 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
override fun resetAllFeatureFlags() {
applyFeatureFlags(initialFeatureFlags)
}
override var isDeleteSitePermissionsEnabled: Boolean by updatedFeatureFlags::isDeleteSitePermissionsEnabled
private fun applyFeatureFlags(featureFlags: FeatureFlags) {
@ -80,6 +90,10 @@ class FeatureSettingsHelperDelegate : FeatureSettingsHelper {
settings.shouldShowTotalCookieProtectionCFR = featureFlags.isTCPCFREnabled
settings.showWallpaperOnboarding = featureFlags.isWallpaperOnboardingEnabled
settings.deleteSitePermissions = featureFlags.isDeleteSitePermissionsEnabled
settings.shouldShowOpenInAppBanner = featureFlags.isOpenInAppBannerEnabled
settings.enableTabsTrayToCompose = featureFlags.tabsTrayRewriteEnabled
settings.enableComposeTopSites = featureFlags.composeTopSitesEnabled
settings.enableTranslations = featureFlags.translationsEnabled
setETPPolicy(featureFlags.etpPolicy)
}
}
@ -95,7 +109,11 @@ private data class FeatureFlags(
var isTCPCFREnabled: Boolean,
var isWallpaperOnboardingEnabled: Boolean,
var isDeleteSitePermissionsEnabled: Boolean,
var isOpenInAppBannerEnabled: Boolean,
var etpPolicy: ETPPolicy,
var tabsTrayRewriteEnabled: Boolean,
var composeTopSitesEnabled: Boolean,
var translationsEnabled: Boolean,
)
internal fun getETPPolicy(settings: Settings): ETPPolicy {

@ -8,15 +8,19 @@ package org.mozilla.fenix.helpers
import android.content.Intent
import android.view.ViewConfiguration.getLongPressTimeout
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.rule.ActivityTestRule
import androidx.test.uiautomator.UiSelector
import org.junit.rules.TestRule
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.helpers.FeatureSettingsHelper.Companion.settings
import org.mozilla.fenix.helpers.TestHelper.appContext
import org.mozilla.fenix.helpers.TestHelper.mDevice
import org.mozilla.fenix.onboarding.FenixOnboarding
typealias HomeActivityComposeTestRule = AndroidComposeTestRule<out TestRule, HomeActivity>
/**
* A [org.junit.Rule] to handle shared test set up for tests on [HomeActivity].
*
@ -48,7 +52,10 @@ class HomeActivityTestRule(
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -59,7 +66,10 @@ class HomeActivityTestRule(
this.isTCPCFREnabled = isTCPCFREnabled
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.composeTopSitesEnabled = composeTopSitesEnabled
}
/**
@ -67,7 +77,7 @@ class HomeActivityTestRule(
*/
fun applySettingsExceptions(settings: (FeatureSettingsHelper) -> Unit) {
FeatureSettingsHelperDelegate().also {
settings(it)
settings(this)
applyFlagUpdates()
}
}
@ -103,14 +113,19 @@ class HomeActivityTestRule(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false,
) = HomeActivityTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
skipOnboarding = skipOnboarding,
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
isOpenInAppBannerEnabled = false,
composeTopSitesEnabled = composeTopSitesEnabled,
)
}
}
@ -146,7 +161,11 @@ class HomeActivityIntentTestRule internal constructor(
isTCPCFREnabled: Boolean = settings.shouldShowTotalCookieProtectionCFR,
isWallpaperOnboardingEnabled: Boolean = settings.showWallpaperOnboarding,
isDeleteSitePermissionsEnabled: Boolean = settings.deleteSitePermissions,
isOpenInAppBannerEnabled: Boolean = settings.shouldShowOpenInAppBanner,
etpPolicy: ETPPolicy = getETPPolicy(settings),
tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) : this(initialTouchMode, launchActivity, skipOnboarding) {
this.isHomeOnboardingDialogEnabled = isHomeOnboardingDialogEnabled
this.isPocketEnabled = isPocketEnabled
@ -157,7 +176,11 @@ class HomeActivityIntentTestRule internal constructor(
this.isTCPCFREnabled = isTCPCFREnabled
this.isWallpaperOnboardingEnabled = isWallpaperOnboardingEnabled
this.isDeleteSitePermissionsEnabled = isDeleteSitePermissionsEnabled
this.isOpenInAppBannerEnabled = isOpenInAppBannerEnabled
this.etpPolicy = etpPolicy
this.tabsTrayRewriteEnabled = tabsTrayRewriteEnabled
this.composeTopSitesEnabled = composeTopSitesEnabled
this.isTranslationsEnabled = translationsEnabled
}
private val longTapUserPreference = getLongPressTimeout()
@ -218,6 +241,7 @@ class HomeActivityIntentTestRule internal constructor(
isTCPCFREnabled = settings.shouldShowTotalCookieProtectionCFR
isWallpaperOnboardingEnabled = settings.showWallpaperOnboarding
isDeleteSitePermissionsEnabled = settings.deleteSitePermissions
isOpenInAppBannerEnabled = settings.shouldShowOpenInAppBanner
etpPolicy = getETPPolicy(settings)
}
@ -236,14 +260,21 @@ class HomeActivityIntentTestRule internal constructor(
initialTouchMode: Boolean = false,
launchActivity: Boolean = true,
skipOnboarding: Boolean = false,
tabsTrayRewriteEnabled: Boolean = false,
composeTopSitesEnabled: Boolean = false,
translationsEnabled: Boolean = false,
) = HomeActivityIntentTestRule(
initialTouchMode = initialTouchMode,
launchActivity = launchActivity,
skipOnboarding = skipOnboarding,
tabsTrayRewriteEnabled = tabsTrayRewriteEnabled,
isJumpBackInCFREnabled = false,
isPWAsPromptEnabled = false,
isTCPCFREnabled = false,
isWallpaperOnboardingEnabled = false,
isOpenInAppBannerEnabled = false,
composeTopSitesEnabled = composeTopSitesEnabled,
translationsEnabled = translationsEnabled,
)
}
}

@ -4,10 +4,14 @@
package org.mozilla.fenix.helpers
import android.util.Log
import androidx.test.uiautomator.UiObject
import androidx.test.uiautomator.UiSelector
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.helpers.Constants.TAG
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTimeShort
import org.mozilla.fenix.helpers.TestHelper.mDevice
/**
@ -15,71 +19,131 @@ import org.mozilla.fenix.helpers.TestHelper.mDevice
*/
object MatcherHelper {
fun itemWithResId(resourceId: String) =
mDevice.findObject(UiSelector().resourceId(resourceId))
fun itemWithResId(resourceId: String): UiObject {
Log.i(TAG, "Looking for item with resource id: $resourceId")
return mDevice.findObject(UiSelector().resourceId(resourceId))
}
fun itemContainingText(itemText: String): UiObject {
Log.i(TAG, "Looking for item with text: $itemText")
return mDevice.findObject(UiSelector().textContains(itemText))
}
fun itemContainingText(itemText: String) =
mDevice.findObject(UiSelector().textContains(itemText))
fun itemWithText(itemText: String): UiObject {
Log.i(TAG, "Looking for item with text: $itemText")
return mDevice.findObject(UiSelector().text(itemText))
}
fun itemWithDescription(description: String) =
mDevice.findObject(UiSelector().descriptionContains(description))
fun itemWithDescription(description: String): UiObject {
Log.i(TAG, "Looking for item with description: $description")
return mDevice.findObject(UiSelector().descriptionContains(description))
}
fun checkedItemWithResId(resourceId: String, isChecked: Boolean) =
mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked))
fun itemWithIndex(index: Int): UiObject {
Log.i(TAG, "Looking for item with index: $index")
return mDevice.findObject(UiSelector().index(index))
}
fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean) =
mDevice.findObject(
fun itemWithClassName(className: String): UiObject {
Log.i(TAG, "Looking for item with class name: $className")
return mDevice.findObject(UiSelector().className(className))
}
fun itemWithResIdAndIndex(resourceId: String, index: Int): UiObject {
Log.i(TAG, "Looking for item with resource id: $resourceId and index: $index")
return mDevice.findObject(UiSelector().resourceId(resourceId).index(index))
}
fun itemWithClassNameAndIndex(className: String, index: Int): UiObject {
Log.i(TAG, "Looking for item with class name: $className and index: $index")
return mDevice.findObject(UiSelector().className(className).index(index))
}
fun checkedItemWithResId(resourceId: String, isChecked: Boolean): UiObject {
Log.i(TAG, "Looking for checked item with resource id: $resourceId")
return mDevice.findObject(UiSelector().resourceId(resourceId).checked(isChecked))
}
fun checkedItemWithResIdAndText(resourceId: String, text: String, isChecked: Boolean): UiObject {
Log.i(TAG, "Looking for checked item with resource id: $resourceId and text: $text")
return mDevice.findObject(
UiSelector()
.resourceId(resourceId)
.textContains(text)
.checked(isChecked),
)
}
fun itemWithResIdAndDescription(resourceId: String, description: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
fun itemWithResIdAndText(resourceId: String, text: String) =
mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
fun itemWithResIdAndDescription(resourceId: String, description: String): UiObject {
Log.i(TAG, "Looking for item with resource id: $resourceId and description: $description")
return mDevice.findObject(UiSelector().resourceId(resourceId).descriptionContains(description))
}
fun assertItemWithResIdExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
fun itemWithResIdAndText(resourceId: String, text: String): UiObject {
Log.i(TAG, "Looking for item with resource id: $resourceId and text: $text")
return mDevice.findObject(UiSelector().resourceId(resourceId).text(text))
}
fun assertItemContainingTextExists(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
}
fun itemWithResIdContainingText(resourceId: String, text: String): UiObject {
Log.i(TAG, "Looking for item with resource id: $resourceId and containing text: $text")
return mDevice.findObject(UiSelector().resourceId(resourceId).textContains(text))
}
fun assertItemWithDescriptionExists(vararg appItems: UiObject) {
fun assertUIObjectExists(
vararg appItems: UiObject,
exists: Boolean = true,
waitingTime: Long = TestAssetHelper.waitingTime,
) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
if (exists) {
assertTrue("${appItem.selector} does not exist", appItem.waitForExists(waitingTime))
Log.i(TAG, "assertUIObjectExists: Verified ${appItem.selector} exists")
} else {
assertFalse("${appItem.selector} exists", appItem.waitForExists(waitingTimeShort))
Log.i(TAG, "assertUIObjectExists: Verified ${appItem.selector} does not exist")
}
}
}
fun assertCheckedItemWithResIdExists(vararg appItems: UiObject) {
fun assertUIObjectIsGone(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
assertTrue("${appItem.selector} is not gone", appItem.waitUntilGone(waitingTime))
Log.i(TAG, "assertUIObjectIsGone: Verified ${appItem.selector} is gone")
}
}
fun assertCheckedItemWithResIdAndTextExists(vararg appItems: UiObject) {
fun assertItemTextEquals(vararg appItems: UiObject, expectedText: String, isEqual: Boolean = true) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
if (isEqual) {
assertTrue(
"${appItem.selector} text does not equal to $expectedText",
appItem.text.equals(expectedText),
)
Log.i(TAG, "assertItemTextEquals: Verified ${appItem.selector} text equals to $expectedText")
} else {
assertFalse(
"${appItem.selector} text equals to $expectedText",
appItem.text.equals(expectedText),
)
Log.i(TAG, "assertItemTextEquals: Verified ${appItem.selector} text does not equal to $expectedText")
}
}
}
fun assertItemWithResIdAndDescriptionExists(vararg appItems: UiObject) {
fun assertItemTextContains(vararg appItems: UiObject, itemText: String) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
assertTrue(
"${appItem.selector} text does not contain $itemText",
appItem.text.contains(itemText),
)
Log.i(TAG, "assertItemTextContains: Verified ${appItem.selector} text contains $itemText")
}
}
fun assertItemWithResIdAndTextExists(vararg appItems: UiObject) {
fun assertItemIsEnabledAndVisible(vararg appItems: UiObject) {
for (appItem in appItems) {
assertTrue(appItem.waitForExists(waitingTime))
assertTrue(appItem.waitForExists(waitingTime) && appItem.isEnabled)
Log.i(TAG, "assertItemIsEnabledAndVisible: Verified ${appItem.selector} is visible and enabled")
}
}
}

@ -7,8 +7,10 @@ package org.mozilla.fenix.helpers
import android.graphics.Bitmap
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.BoundedMatcher
import androidx.test.espresso.matcher.ViewMatchers
import junit.framework.AssertionFailedError
import org.hamcrest.CoreMatchers.not
@ -70,3 +72,21 @@ fun ViewInteraction.isVisibleForUser(): Boolean {
return true
}
fun atPosition(position: Int, itemMatcher: Matcher<View?>): Matcher<View?>? {
return object : BoundedMatcher<View?, RecyclerView>(
RecyclerView::class.java,
) {
override fun describeTo(description: Description) {
description.appendText("has item at position $position: ")
itemMatcher.describeTo(description)
}
override fun matchesSafely(view: RecyclerView): Boolean {
val viewHolder = view.findViewHolderForAdapterPosition(position)
?: // has no item on such position
return false
return itemMatcher.matches(viewHolder.itemView)
}
}
}

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

Loading…
Cancel
Save