Merge tag 'v87.0.0-rc.1' into fork

pull/295/head
Adam Novak 3 years ago
commit af23310679

@ -18,6 +18,6 @@ assignees: ''
### Device information ### Device information
* Android device: ? * Device vendor / model and Android version: ?
* Fenix version: ? * Firefox for Android version: ? (go to Settings -> About Firefox)

62
.github/stale.yml vendored

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

@ -0,0 +1,103 @@
name: Android build PR
on: [pull_request]
jobs:
run-build:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: "Clean & Assemble Debug"
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: clean app:assembleDebug
run-testDebugUnitTest:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: "Test Debug Unit Tests"
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: testDebugUnitTest
run-detekt:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: "Detekt"
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: detekt
- name: Archive detekt results
uses: actions/upload-artifact@v2
with:
name: detekt report
path: build/reports/detekt.html
run-ktlint:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: "Ktlint"
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: ktlint
run-lintDebug:
runs-on: ubuntu-20.04
if: github.event.pull_request.head.repo.full_name != github.repository
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Setup Java
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: "Lint Debug"
uses: eskatos/gradle-command-action@v1
with:
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
arguments: lintDebug
- name: Archive lint results
uses: actions/upload-artifact@v2
with:
name: lintDebug report
path: app/build/reports/lint-results-debug.html

@ -21,7 +21,7 @@ pull_request_rules:
conditions: conditions:
- author=mozilla-l10n-automation-bot - author=mozilla-l10n-automation-bot
- status-success=pr-complete - status-success=pr-complete
- files~=(strings.xml) - files~=(strings.xml|l10n.toml)
actions: actions:
review: review:
type: APPROVE type: APPROVE

@ -103,7 +103,7 @@ tasks:
$if: > $if: >
tasks_for in ["action", "cron"] tasks_for in ["action", "cron"]
|| (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"]) || (tasks_for == "github-pull-request" && pullRequestAction in ["opened", "reopened", "synchronize"])
|| (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") || (tasks_for == "github-push" && head_branch[:10] != "refs/tags/") && (head_branch != "staging.tmp") && (head_branch != "trying.tmp") && (head_branch[:8] != "mergify/")
|| (tasks_for == "github-release" && releaseAction == "published" && (ownerEmail != "mozilla-release-automation-bot@users.noreply.github.com") && (ownerEmail != "mozilla-release-automation-bot-staging@users.noreply.github.com")) || (tasks_for == "github-release" && releaseAction == "published" && (ownerEmail != "mozilla-release-automation-bot@users.noreply.github.com") && (ownerEmail != "mozilla-release-automation-bot-staging@users.noreply.github.com"))
then: then:
$let: $let:
@ -166,7 +166,7 @@ tasks:
routes: routes:
$flattenDeep: $flattenDeep:
- checks - checks
- $if: 'level == "3"' - $if: 'level == "3" || repoUrl == "https://github.com/mozilla-releng/staging-fenix"'
then: then:
- tc-treeherder.v2.${project}.${head_sha} - tc-treeherder.v2.${project}.${head_sha}
# TODO Bug 1601928: Make this scope fork-friendly once ${project} is better defined. This will enable # TODO Bug 1601928: Make this scope fork-friendly once ${project} is better defined. This will enable

@ -41,12 +41,25 @@ android {
buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\"" buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\"" buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\"" buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\""
// These add-ons should be excluded for Mozilla Online builds.
buildConfigField "String[]", "MOZILLA_ONLINE_ADDON_EXCLUSIONS",
"{" +
"\"uBlock0@raymondhill.net\"," +
"\"firefox@ghostery.com\"," +
"\"jid1-MnnxcxisBPnSXQ@jetpack\"," +
"\"adguardadblocker@adguard.com\"," +
"\"foxyproxy@eric.h.jung\"," +
"\"{73a6fe31-595d-460b-a920-fcc0f8843232}\"," +
"\"jid1-BoFifL9Vbdl2zQ@jetpack\"," +
"\"woop-NoopscooPsnSXQ@jetpack\"" +
"}"
// This should be the base URL used to call the AMO API. // This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\"" buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "fenix-dev" def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [ manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
] ]
// Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`. // Build flag for "Mozilla Online" variants. See `Config.isMozillaOnline`.
@ -81,13 +94,19 @@ android {
applicationIdSuffix ".fenix.debug" applicationIdSuffix ".fenix.debug"
resValue "bool", "IS_DEBUG", "true" resValue "bool", "IS_DEBUG", "true"
pseudoLocalesEnabled true pseudoLocalesEnabled true
def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
"deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": false
]
} }
nightly releaseTemplate >> { nightly releaseTemplate >> {
applicationIdSuffix ".fenix" applicationIdSuffix ".fenix"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
def deepLinkSchemeValue = "fenix-nightly" def deepLinkSchemeValue = "fenix-nightly"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue] manifestPlaceholders = ["deepLinkScheme": deepLinkSchemeValue, "requestLegacyExternalStorage": false]
} }
beta releaseTemplate >> { beta releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
@ -103,7 +122,8 @@ android {
// - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922 // - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID", "sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
] ]
} }
release releaseTemplate >> { release releaseTemplate >> {
@ -120,7 +140,8 @@ android {
// - https://issuetracker.google.com/issues/36924841 // - https://issuetracker.google.com/issues/36924841
// - https://issuetracker.google.com/issues/36905922 // - https://issuetracker.google.com/issues/36905922
"sharedUserId": "org.mozilla.firefox.sharedID", "sharedUserId": "org.mozilla.firefox.sharedID",
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue,
"requestLegacyExternalStorage": true
] ]
} }
forkDebug { forkDebug {
@ -398,6 +419,16 @@ android.applicationVariants.all { variant ->
buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null' buildConfigField 'String', 'NIMBUS_ENDPOINT', 'null'
println("--") println("--")
} }
// -------------------------------------------------------------------------------------------------
// BuildConfig: Set flag for official builds; similar to MOZILLA_OFFICIAL in mozilla-central.
// -------------------------------------------------------------------------------------------------
if (project.hasProperty("official") || gradle.hasProperty("localProperties.official")) {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'true'
} else {
buildConfigField 'Boolean', 'MOZILLA_OFFICIAL', 'false'
}
} }
androidExtensions { androidExtensions {
@ -507,6 +538,7 @@ dependencies {
implementation Deps.mozilla_feature_top_sites implementation Deps.mozilla_feature_top_sites
implementation Deps.mozilla_feature_share implementation Deps.mozilla_feature_share
implementation Deps.mozilla_feature_accounts_push implementation Deps.mozilla_feature_accounts_push
implementation Deps.mozilla_feature_webauthn
implementation Deps.mozilla_feature_webcompat implementation Deps.mozilla_feature_webcompat
implementation Deps.mozilla_feature_webnotifications implementation Deps.mozilla_feature_webnotifications
implementation Deps.mozilla_feature_webcompat_reporter implementation Deps.mozilla_feature_webcompat_reporter

@ -234,6 +234,25 @@ events:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
normal_and_private_uri_count:
type: counter
description: |
A counter of URIs visited by the user in the current session, including
page reloads. This includes private browsing. This does not include
background page requests and URIs from embedded pages but may be
incremented without user interaction by website scripts that
programmatically redirect to a new location.
send_in_pings:
- metrics
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17089
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17935
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2022-08-01"
preference_toggled: preference_toggled:
type: event type: event
description: | description: |
@ -623,7 +642,7 @@ login_dialog:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
cancelled: cancelled:
type: event type: event
description: | description: |
@ -636,7 +655,7 @@ login_dialog:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
saved: saved:
type: event type: event
description: | description: |
@ -649,7 +668,7 @@ login_dialog:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
never_save: never_save:
type: event type: event
description: | description: |
@ -662,7 +681,7 @@ login_dialog:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
find_in_page: find_in_page:
opened: opened:
@ -3859,14 +3878,21 @@ addons:
expires: "2021-04-01" expires: "2021-04-01"
startup.timeline: startup.timeline:
framework_start: framework_primary:
send_in_pings: send_in_pings:
- startup-timeline - startup-timeline
type: timespan type: timespan
time_unit: nanosecond time_unit: millisecond
description: | description: |
The duration the Android framework takes to start before letting us run The duration the Android framework takes to start before letting us run
code in `*Application.init`. This is calculated from `appInitTimestamp - code in `*Application.init` when this device has `clock_ticks_per_second`
equal to 100: if it's not equal to 100, then this value is captured in
`framework_secondary`. We split this into two metrics to make it easier
to analyze in GLAM. We split on 100 because when we did our initial brief
analysis - https://sql.telemetry.mozilla.org/queries/75591 - the results
for clocks ticks were overwhelmingly 100.
The duration is calculated from `appInitTimestamp -
processStartTimestamp`. `processStartTimestamp` is derived from the clock processStartTimestamp`. `processStartTimestamp` is derived from the clock
tick time unit, which is expected to be less granular than nanoseconds. tick time unit, which is expected to be less granular than nanoseconds.
Therefore, we convert and round our timestamps to clock ticks before Therefore, we convert and round our timestamps to clock ticks before
@ -3876,9 +3902,30 @@ startup.timeline:
devices, is also reported as a metric devices, is also reported as a metric
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/8803 - https://github.com/mozilla-mobile/fenix/issues/8803
- https://github.com/mozilla-mobile/fenix/issues/17972
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626 - https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626
- https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068 - https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068
- https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-01"
framework_secondary:
send_in_pings:
- startup-timeline
type: timespan
time_unit: millisecond
description: |
The duration the Android framework takes to start before letting us run
code in `*Application.init` when this device has `clock_ticks_per_second`
not equal to 100. For more details on this metric, see `framework_primary`
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17972
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284
data_sensitivity: data_sensitivity:
- technical - technical
notification_emails: notification_emails:
@ -3890,9 +3937,9 @@ startup.timeline:
- startup-timeline - startup-timeline
type: boolean type: boolean
description: | description: |
An error when attempting to record `framework_start` - the application An error when attempting to record `framework_primary/secondary` - the
init timestamp returned a negative value - which is likely indicative of a application init timestamp returned a negative value - which is likely
bug in the implementation. indicative of a bug in the implementation.
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/8803 - https://github.com/mozilla-mobile/fenix/issues/8803
data_reviews: data_reviews:
@ -3943,6 +3990,38 @@ startup.timeline:
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
perf.startup:
application_on_create:
type: timing_distribution
time_unit: millisecond
description: |
The duration of `FenixApplication.onCreate` in the main process.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17969
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
home_activity_on_create:
type: timing_distribution
time_unit: millisecond
description: |
The duration of `HomeActivity.onCreate`.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17969
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17973#issue-572183889
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
perf.awesomebar: perf.awesomebar:
history_suggestions: history_suggestions:
send_in_pings: send_in_pings:
@ -4083,7 +4162,7 @@ autoplay:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
setting_changed: setting_changed:
type: event type: event
description: | description: |
@ -4102,7 +4181,7 @@ autoplay:
- interaction - interaction
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
storage.stats: storage.stats:
query_stats_duration: query_stats_duration:
@ -4308,6 +4387,47 @@ tabs:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
banner_open_in_app:
displayed:
type: event
description: |
Open in App banner was shown.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16828
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17049
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
dismissed:
type: event
description: |
User tapped 'dismiss' on Open in App banner.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16828
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17049
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
go_to_settings:
type: event
description: |
User tapped 'go to settings' on Open in App banner.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16828
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17049
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
contextual_menu: contextual_menu:
copy_tapped: copy_tapped:
type: event type: event
@ -4361,3 +4481,156 @@ contextual_menu:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-06-01" expires: "2021-06-01"
engine:
tab_kills:
type: labeled_counter
labels:
- foreground
- background
description: |
How often was the content process of a foreground (selected) or
background tab killed.
bugs:
- https://github.com/mozilla-mobile/android-components/issues/9366
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17864
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
kill_foreground_age:
type: timespan
time_unit: millisecond
description: |
Measures the age of the engine session of a foreground (selected) tab
at the time its content process got killed.
bugs:
- https://github.com/mozilla-mobile/android-components/issues/9366
data_reviews:
- TBD
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
kill_background_age:
type: timespan
time_unit: millisecond
description: |
Measures the age of the engine session of a background tab at the
time its content process got killed.
bugs:
- https://github.com/mozilla-mobile/android-components/issues/9366
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17864
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
android_keystore_experiment:
experiment_failure:
type: event
description: |
Records an instance of an unexpected failure during the experiment
extra_keys:
failure_exception:
description: |
Exception class associated with an unexpected failure of this
experiment, not caught by the other failure handlers.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"
get_failure:
type: event
description: |
Unexpected failure when trying to read from secure prefs.
extra_keys:
failure_exception:
description: |
Exception class associated with an unexpected failure of this
experiment.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"
get_result:
type: event
description: |
Success when trying to read from secure prefs.
extra_keys:
result:
description: |
Result code identifying whether the read operation returned the
expected value or not.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"
write_failure:
type: event
description: |
Unexpected failure when trying to write to secure prefs.
extra_keys:
failure_exception:
description: |
Exception class associated with an unexpected failure of this
experiment.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"
write_success:
type: event
description: |
Success in writing to secure prefs.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"
reset:
type: event
description: |
An experiment failed, and was reset to run again in the future from a
blank state.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17869
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18333#pullrequestreview-612447395
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
expires: "2021-09-01"

@ -12,8 +12,8 @@ activation:
an hashed version of the Google Advertising ID. an hashed version of the Google Advertising ID.
include_client_id: false include_client_id: false
bugs: bugs:
- 1538011 - https://bugzilla.mozilla.com/1538011/
- 1501822 - https://bugzilla.mozilla.com/1501822/
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209 - https://github.com/mozilla-mobile/fenix/pull/1707#issuecomment-486972209
notification_emails: notification_emails:
@ -35,14 +35,19 @@ startup-timeline:
description: | description: |
This ping is intended to provide an understanding of startup performance. This ping is intended to provide an understanding of startup performance.
The ping is intended to be captured by performance testing automation to In addition to being captured on real devices, the ping data was prematurely
report results there, in addition to user telemetry. We place these metrics optimized into this separate ping to be isolated from other metrics to be
into their own ping in order to isolate them and make this process easier. more easily captured by performance testing automation but that hasn't
include_client_id: false happened in practice. We would have removed it but implementation
details don't make that possible:
https://github.com/mozilla-mobile/fenix/issues/17972#issuecomment-781002987
include_client_id: true
bugs: bugs:
- https://github.com/mozilla-mobile/fenix/issues/8803 - https://github.com/mozilla-mobile/fenix/issues/8803
- https://github.com/mozilla-mobile/fenix/issues/17972
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626 - https://github.com/mozilla-mobile/fenix/pull/9788#pullrequestreview-394228626
- https://github.com/mozilla-mobile/fenix/pull/18043#issue-575389284
notification_emails: notification_emails:
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- esmyth@mozilla.com - mcomella@mozilla.com

@ -9,5 +9,71 @@
</head> </head>
<body> <body>
<iframe src="http://trackertest.org/"></iframe> <iframe src="http://trackertest.org/"></iframe>
<h3>Level 1 (Basic) List</h3>
<p>social-track-digest256:</p>
<img
src="https://social-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<br/>
<p>ads-track-digest256:</p>
<img
src="https://ads-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<br/>
<p>analytics-track-digest256:</p>
<img
src="https://analytics-track-digest256.dummytracker.org/test_not_blocked.png"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png'">
<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='blocked';"
onload="this.onload=null;var result=document.getElementById('result');result.innerHTML='NOT blocked';"
></script>
</p>
<br/>
<p>Cryptomining:
<img
src="https://base-cryptomining-track-digest256.dummytracker.org/test_not_blocked.png" alt="not blocked"
onerror="this.onerror=null;this.src='https://not-a-tracker.dummytracker.org/test_blocked.png';this.alt='blocked'">
</p>
<p><b>Cookie blocking</b>
</p>
<iframe height=0 width=0 src="https://social-tracking-protection-facebook-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<iframe height=0 width=0 src="https://social-tracking-protection-linkedin-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<iframe height=0 width=0 src="https://social-tracking-protection-twitter-digest256.dummytracker.org/cookie_access_test.html?test_origin=senglehardt.com"></iframe>
<p>
* Facebook-cookies <pre id="social-tracking-protection-facebook-digest256"></pre>
* LinkedIn-cookies <pre id="social-tracking-protection-linkedin-digest256"></pre>
* Twitter-cookies <pre id="social-tracking-protection-twitter-digest256"></pre>
</p>
<script>
function updateCookieStatus(statusMessage, list) {
var output = document.getElementById(list);
if (statusMessage === 'cookies') {
output.innerHTML = "Cookies not blocked";
} else if (statusMessage === 'no_cookies') {
output.innerHTML = "Blocked";
} else {
output.innerHTML = "Unrecognized status";
}
}
window.addEventListener("message", event => {
lists = [
'social-tracking-protection-facebook-digest256',
'social-tracking-protection-linkedin-digest256',
'social-tracking-protection-twitter-digest256'
];
lists.forEach(list => {
if (event.origin === `https://${list}.dummytracker.org`) {
updateCookieStatus(event.data, list);
}
});
}, false);
</script>
</body> </body>
</html> </html>

@ -152,7 +152,7 @@ class BaselinePingTest {
.click() .click()
// Validate the received data. // Validate the received data.
val baselinePing = waitForPingContent("baseline", "background")!! val baselinePing = waitForPingContent("baseline", "inactive")!!
val metrics = baselinePing.getJSONObject("metrics") val metrics = baselinePing.getJSONObject("metrics")

@ -4,6 +4,7 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.view.ViewConfiguration.getLongPressTimeout
import androidx.test.espresso.intent.rule.IntentsTestRule import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
@ -25,11 +26,18 @@ class HomeActivityTestRule(
private val skipOnboarding: Boolean = false private val skipOnboarding: Boolean = false
) : ) :
ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) { ActivityTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
private val longTapUserPreference = getLongPressTimeout()
override fun beforeActivityLaunched() { override fun beforeActivityLaunched() {
super.beforeActivityLaunched() super.beforeActivityLaunched()
setLongTapTimeout() setLongTapTimeout(3000)
if (skipOnboarding) { skipOnboardingBeforeLaunch() } if (skipOnboarding) { skipOnboardingBeforeLaunch() }
} }
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
}
} }
/** /**
@ -46,17 +54,24 @@ class HomeActivityIntentTestRule(
private val skipOnboarding: Boolean = false private val skipOnboarding: Boolean = false
) : ) :
IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) { IntentsTestRule<HomeActivity>(HomeActivity::class.java, initialTouchMode, launchActivity) {
private val longTapUserPreference = getLongPressTimeout()
override fun beforeActivityLaunched() { override fun beforeActivityLaunched() {
super.beforeActivityLaunched() super.beforeActivityLaunched()
setLongTapTimeout() setLongTapTimeout(3000)
if (skipOnboarding) { skipOnboardingBeforeLaunch() } if (skipOnboarding) { skipOnboardingBeforeLaunch() }
} }
override fun afterActivityFinished() {
super.afterActivityFinished()
setLongTapTimeout(longTapUserPreference)
}
} }
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click // changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
fun setLongTapTimeout() { fun setLongTapTimeout(delay: Int) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 3000") mDevice.executeShellCommand("settings put secure long_press_timeout $delay")
} }
private fun skipOnboardingBeforeLaunch() { private fun skipOnboardingBeforeLaunch() {

@ -5,6 +5,7 @@ package org.mozilla.fenix.helpers
import androidx.test.core.app.ApplicationProvider import androidx.test.core.app.ApplicationProvider
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import mozilla.components.browser.state.selector.selectedTab
import org.mozilla.fenix.FenixApplication import org.mozilla.fenix.FenixApplication
/** /**
@ -21,13 +22,12 @@ class SessionLoadedIdlingResource : IdlingResource {
override fun isIdleNow(): Boolean { override fun isIdleNow(): Boolean {
val context = ApplicationProvider.getApplicationContext<FenixApplication>() val context = ApplicationProvider.getApplicationContext<FenixApplication>()
val sessionManager = context.components.core.sessionManager val selectedTab = context.components.core.store.state.selectedTab
val session = sessionManager.selectedSession
return if (session?.loading == true) { return if (selectedTab?.content?.loading == true) {
false false
} else { } else {
if (session?.progress == 100) { if (selectedTab?.content?.progress == 100) {
invokeCallback() invokeCallback()
true true
} else { } else {

@ -12,11 +12,6 @@ import android.os.Build
import android.os.Environment import android.os.Environment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.CoordinatesProvider
import androidx.test.espresso.action.GeneralClickAction
import androidx.test.espresso.action.Press
import androidx.test.espresso.action.Tap
import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
@ -108,22 +103,6 @@ object TestHelper {
} }
} }
fun sendSingleTapToScreen(x: Int, y: Int): ViewAction? {
return GeneralClickAction(
Tap.SINGLE,
CoordinatesProvider { view ->
val screenPos = IntArray(2)
view.getLocationOnScreen(screenPos)
val screenX = screenPos[0] + x.toFloat()
val screenY = screenPos[1] + y.toFloat()
floatArrayOf(screenX, screenY)
},
Press.FINGER,
0,
0
)
}
// Remove test file from the device Downloads folder // Remove test file from the device Downloads folder
@Suppress("Deprecation") @Suppress("Deprecation")
fun deleteDownloadFromStorage(fileName: String) { fun deleteDownloadFromStorage(fileName: String) {

@ -4,7 +4,14 @@
package org.mozilla.fenix.helpers package org.mozilla.fenix.helpers
import android.view.InputDevice
import android.view.MotionEvent
import androidx.test.espresso.ViewAction
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.GeneralClickAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.Press
import androidx.test.espresso.action.Tap
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
@ -21,3 +28,21 @@ fun ViewInteraction.assertIsChecked(isChecked: Boolean): ViewInteraction {
fun ViewInteraction.assertIsSelected(isSelected: Boolean): ViewInteraction { fun ViewInteraction.assertIsSelected(isSelected: Boolean): ViewInteraction {
return this.check(matches(isSelected(isSelected)))!! return this.check(matches(isSelected(isSelected)))!!
} }
/**
* Perform a click (simulate the finger touching the View) at a specific location in the View
* rather than the default middle of the View.
*
* Useful in situations where the View we want clicked contains other Views in it's x,y middle
* and we need to simulate the touch in some other free space of the View we want clicked.
*/
fun ViewInteraction.clickAtLocationInView(locationInView: GeneralLocation): ViewAction =
ViewActions.actionWithAssertions(
GeneralClickAction(
Tap.SINGLE,
locationInView,
Press.FINGER,
InputDevice.SOURCE_UNKNOWN,
MotionEvent.BUTTON_PRIMARY
)
)

@ -44,10 +44,14 @@ class DefaultHomeScreenTest : ScreenshotTest() {
SystemClock.sleep(TestAssetHelper.waitingTimeShort) SystemClock.sleep(TestAssetHelper.waitingTimeShort)
Screengrab.screenshot("HomeScreenRobot_home-screen") Screengrab.screenshot("HomeScreenRobot_home-screen")
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { } }.openSettings {
}.openPrivateBrowsingSubMenu {
clickPrivateModeScreenshotsSwitch()
}
// To get private screenshot, // To get private screenshot,
// dismiss onboarding going to settings and back // dismiss onboarding going to settings and back
mDevice.pressBack() mDevice.pressBack()
mDevice.pressBack()
homeScreen { homeScreen {
togglePrivateBrowsingModeOnOff() togglePrivateBrowsingModeOnOff()
Screengrab.screenshot("HomeScreenRobot_private-browsing-menu") Screengrab.screenshot("HomeScreenRobot_private-browsing-menu")

@ -494,12 +494,16 @@ class BookmarksTest {
}.openBookmarks { }.openBookmarks {
createFolder("1") createFolder("1")
getInstrumentation().waitForIdleSync() getInstrumentation().waitForIdleSync()
waitForBookmarksFolderContentToExist("Bookmarks", "1")
selectFolder("1") selectFolder("1")
verifyCurrentFolderTitle("1")
createFolder("2") createFolder("2")
getInstrumentation().waitForIdleSync() getInstrumentation().waitForIdleSync()
waitForBookmarksFolderContentToExist("1", "2")
selectFolder("2") selectFolder("2")
verifyCurrentFolderTitle("2") verifyCurrentFolderTitle("2")
navigateUp() navigateUp()
waitForBookmarksFolderContentToExist("1", "2")
verifyCurrentFolderTitle("1") verifyCurrentFolderTitle("1")
mDevice.pressBack() mDevice.pressBack()
verifyBookmarksMenuView() verifyBookmarksMenuView()

@ -69,6 +69,7 @@ class ContextMenusTest {
snackBarButtonClick("Switch") snackBarButtonClick("Switch")
verifyUrl(genericURL.url.toString()) verifyUrl(genericURL.url.toString())
}.openTabDrawer { }.openTabDrawer {
verifyNormalModeSelected()
verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_1")
verifyExistingOpenTabs("Test_Page_4") verifyExistingOpenTabs("Test_Page_4")
} }

@ -66,7 +66,6 @@ class NavigationToolbarTest {
} }
} }
@Ignore("Flaky test: https://github.com/mozilla-mobile/fenix/issues/12894")
@Test @Test
fun goForwardTest() { fun goForwardTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -79,7 +78,8 @@ class NavigationToolbarTest {
}.enterURLAndEnterToBrowser(nextWebPage.url) { }.enterURLAndEnterToBrowser(nextWebPage.url) {
mDevice.waitForIdle() mDevice.waitForIdle()
verifyUrl(nextWebPage.url.toString()) verifyUrl(nextWebPage.url.toString())
mDevice.pressBack() }.openThreeDotMenu {
}.goBack {
mDevice.waitForIdle() mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString()) verifyUrl(defaultWebPage.url.toString())
} }

@ -12,11 +12,14 @@ import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import org.junit.Ignore
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.mDevice import org.mozilla.fenix.ui.robots.mDevice
/** /**
@ -28,10 +31,10 @@ import org.mozilla.fenix.ui.robots.mDevice
* *
*/ */
// @Ignore("Temp disable - reader view page detection issues: https://github.com/mozilla-mobile/fenix/issues/9688 ")
class ReaderViewTest { class ReaderViewTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private var readerViewNotification: ViewVisibilityIdlingResource? = null private var readerViewNotification: ViewVisibilityIdlingResource? = null
private val estimatedReadingTime = "1 - 2 minutes"
@get:Rule @get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule() val activityIntentTestRule = HomeActivityIntentTestRule()
@ -101,6 +104,7 @@ class ReaderViewTest {
@Test @Test
fun verifyReaderViewToggle() { fun verifyReaderViewToggle() {
// New three-dot menu design does not have readerview appearance menu item
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -119,18 +123,27 @@ class ReaderViewTest {
navigationToolbar { navigationToolbar {
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
toggleReaderView() toggleReaderView()
}.openThreeDotMenu { mDevice.waitForIdle()
verifyReaderViewAppearance(true) }
}.closeBrowserMenuToBrowser { }
if (!FeatureFlags.toolbarMenuFeature) {
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.closeBrowserMenuToBrowser { }
}
navigationToolbar { navigationToolbar {
toggleReaderView() toggleReaderView()
mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(false) verifyReaderViewAppearance(false)
}.close { } }.close { }
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceFontToggle() { fun verifyReaderViewAppearanceFontToggle() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -150,6 +163,11 @@ class ReaderViewTest {
navigationToolbar { navigationToolbar {
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
toggleReaderView() toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {
@ -166,6 +184,7 @@ class ReaderViewTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceFontSizeToggle() { fun verifyReaderViewAppearanceFontSizeToggle() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -185,6 +204,11 @@ class ReaderViewTest {
navigationToolbar { navigationToolbar {
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
toggleReaderView() toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {
@ -207,6 +231,7 @@ class ReaderViewTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceColorSchemeChange() { fun verifyReaderViewAppearanceColorSchemeChange() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -226,6 +251,11 @@ class ReaderViewTest {
navigationToolbar { navigationToolbar {
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
toggleReaderView() toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {

@ -79,19 +79,23 @@ class SettingsAddonsTest {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val addonName = "uBlock Origin" val addonName = "uBlock Origin"
navigationToolbar { navigationToolbar {}
}.openNewTabAndEnterToBrowser(defaultWebPage.url) { .openNewTabAndEnterToBrowser(defaultWebPage.url) {}
}.openThreeDotMenu { .openThreeDotMenu {}
}.openAddonsManagerMenu { .openAddonsManagerMenu {
addonsListIdlingResource = addonsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.add_ons_list), 1) RecyclerViewIdlingResource(
IdlingRegistry.getInstance().register(addonsListIdlingResource!!) activityTestRule.activity.findViewById(R.id.add_ons_list),
clickInstallAddon(addonName) 1
verifyAddonPrompt(addonName) )
cancelInstallAddon() IdlingRegistry.getInstance().register(addonsListIdlingResource!!)
clickInstallAddon(addonName) clickInstallAddon(addonName)
acceptInstallAddon() verifyAddonPrompt(addonName)
verifyDownloadAddonPrompt(addonName, activityTestRule) cancelInstallAddon()
clickInstallAddon(addonName)
acceptInstallAddon()
verifyDownloadAddonPrompt(addonName, activityTestRule)
} }
} }

@ -27,7 +27,9 @@ import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickTabCrashedRestoreButton
import org.mozilla.fenix.ui.robots.clickUrlbar import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.dismissTrackingOnboarding
import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.enhancedTrackingProtection import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
@ -48,10 +50,6 @@ class SmokeTest {
private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null
private var readerViewNotification: ViewVisibilityIdlingResource? = null private var readerViewNotification: ViewVisibilityIdlingResource? = null
private val downloadFileName = "Globe.svg" private val downloadFileName = "Globe.svg"
private val searchEngine = object {
var title = "Ecosia"
var url = "https://www.ecosia.org/search?q=%s"
}
val collectionName = "First Collection" val collectionName = "First Collection"
private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null
@ -201,6 +199,7 @@ class SmokeTest {
@Test @Test
// Verifies the list of items in a tab's 3 dot menu // Verifies the list of items in a tab's 3 dot menu
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun verifyPageMainMenuItemsTest() { fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -365,6 +364,7 @@ class SmokeTest {
@Test @Test
// Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar // Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun verifyETPShieldNotDisplayedIfOFFGlobally() { fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -372,7 +372,7 @@ class SmokeTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openEnhancedTrackingProtectionSubMenu { }.openEnhancedTrackingProtectionSubMenu {
clickEnhancedTrackingProtectionDefaults() switchEnhancedTrackingProtectionToggle()
verifyEnhancedTrackingProtectionOptionsGrayedOut() verifyEnhancedTrackingProtectionOptionsGrayedOut()
}.goBackToHomeScreen { }.goBackToHomeScreen {
navigationToolbar { navigationToolbar {
@ -381,7 +381,7 @@ class SmokeTest {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openEnhancedTrackingProtectionSubMenu { }.openEnhancedTrackingProtectionSubMenu {
clickEnhancedTrackingProtectionDefaults() switchEnhancedTrackingProtectionToggle()
}.goBack { }.goBack {
}.goBackToBrowser { }.goBackToBrowser {
clickEnhancedTrackingProtectionPanel() clickEnhancedTrackingProtectionPanel()
@ -391,6 +391,32 @@ class SmokeTest {
} }
} }
@Test
fun customTrackingProtectionSettingsTest() {
val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openEnhancedTrackingProtectionSubMenu {
verifyEnhancedTrackingProtectionOptions()
selectTrackingProtectionOption("Custom")
verifyCustomTrackingProtectionSettings()
}.goBackToHomeScreen {}
navigationToolbar {
}.enterURLAndEnterToBrowser(trackingPage.url) {}
enhancedTrackingProtection {
dismissTrackingOnboarding()
}.openEnhancedTrackingProtectionSheet {
verifyTrackingCookiesBlocked()
verifyCryptominersBlocked()
verifyFingerprintersBlocked()
verifyBasicLevelTrackingContentBlocked()
}
}
@Test @Test
// Verifies changing the default engine from the Search Shortcut menu // Verifies changing the default engine from the Search Shortcut menu
fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() { fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() {
@ -495,31 +521,6 @@ class SmokeTest {
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/17847") @Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/17847")
@Test
// Verifies setting as default a customized search engine name and URL
fun editCustomSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
typeCustomEngineDetails(searchEngine.title, searchEngine.url)
saveNewSearchEngine()
openEngineOverflowMenu("Ecosia")
clickEdit()
typeCustomEngineDetails("Test", searchEngine.url)
saveEditSearchEngine()
changeDefaultSearchEngine("Test")
}.goBack {
}.goBack {
}.openSearch {
verifyDefaultSearchEngine("Test")
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains("Test")
}
}
@Test @Test
// Swipes the nav bar left/right to switch between tabs // Swipes the nav bar left/right to switch between tabs
fun swipeToSwitchTabTest() { fun swipeToSwitchTabTest() {
@ -532,14 +533,15 @@ class SmokeTest {
}.openNewTab { }.openNewTab {
}.submitQuery(secondWebPage.url.toString()) { }.submitQuery(secondWebPage.url.toString()) {
swipeNavBarRight(secondWebPage.url.toString()) swipeNavBarRight(secondWebPage.url.toString())
verifyPageContent(firstWebPage.content) verifyUrl(firstWebPage.url.toString())
swipeNavBarLeft(firstWebPage.url.toString()) swipeNavBarLeft(firstWebPage.url.toString())
verifyPageContent(secondWebPage.content) verifyUrl(secondWebPage.url.toString())
} }
} }
@Test @Test
// Saves a login, then changes it and verifies the update // Saves a login, then changes it and verifies the update
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun updateSavedLoginTest() { fun updateSavedLoginTest() {
val saveLoginTest = val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer) TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -603,6 +605,7 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
// Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers // Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers
fun noCrashWithAddonInstalledTest() { fun noCrashWithAddonInstalledTest() {
// setting ETP to Strict mode to test it works with add-ons // setting ETP to Strict mode to test it works with add-ons
@ -1110,6 +1113,7 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
fun mainMenuInstallPWATest() { fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/" val pwaPage = "https://rpappalax.github.io/testapp/"
@ -1126,10 +1130,12 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
// Verifies that reader mode is detected and the custom appearance controls are displayed // Verifies that reader mode is detected and the custom appearance controls are displayed
fun verifyReaderViewAppearanceUI() { fun verifyReaderViewAppearanceUI() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
val estimatedReadingTime = "1 - 2 minutes"
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(readerViewPage.url) { }.enterURLAndEnterToBrowser(readerViewPage.url) {
@ -1146,6 +1152,11 @@ class SmokeTest {
navigationToolbar { navigationToolbar {
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
toggleReaderView() toggleReaderView()
mDevice.waitForIdle()
}
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {
@ -1160,4 +1171,31 @@ class SmokeTest {
verifyAppearanceColorSepia(true) verifyAppearanceColorSepia(true)
} }
} }
@Test
fun closeTabCrashedReporterTest() {
homeScreen {
}.openNavigationToolbar {
}.openTabCrashReporter {
}.clickTabCrashedCloseButton {
}.openTabDrawer {
verifyNoTabsOpened()
}
}
@Test
fun restoreTabCrashedReporterTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {}
navigationToolbar {
}.openTabCrashReporter {
clickTabCrashedRestoreButton()
verifyPageContent(website.content)
}
}
} }

@ -8,6 +8,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -70,7 +71,7 @@ class StrictEnhancedTrackingProtectionTest {
}.openEnhancedTrackingProtectionSubMenu { }.openEnhancedTrackingProtectionSubMenu {
verifyEnhancedTrackingProtectionHeader() verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionOptions() verifyEnhancedTrackingProtectionOptions()
verifyEnhancedTrackingProtectionDefaults() verifyTrackingProtectionSwitchEnabled()
}.openExceptions { }.openExceptions {
verifyDefault() verifyDefault()
} }
@ -126,6 +127,7 @@ class StrictEnhancedTrackingProtectionTest {
} }
@Test @Test
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun testStrictVisitDisable() { fun testStrictVisitDisable() {
val trackingProtectionTest = val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
@ -177,7 +179,7 @@ class StrictEnhancedTrackingProtectionTest {
}.openProtectionSettings { }.openProtectionSettings {
verifyEnhancedTrackingProtectionHeader() verifyEnhancedTrackingProtectionHeader()
verifyEnhancedTrackingProtectionOptions() verifyEnhancedTrackingProtectionOptions()
verifyEnhancedTrackingProtectionDefaults() verifyTrackingProtectionSwitchEnabled()
} }
settingsSubMenuEnhancedTrackingProtection { settingsSubMenuEnhancedTrackingProtection {

@ -10,13 +10,11 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper.sendSingleTapToScreen
import org.mozilla.fenix.ui.robots.browserScreen import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
@ -55,16 +53,6 @@ class TabbedBrowsingTest {
} }
} }
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
companion object {
@BeforeClass
@JvmStatic
fun setDevicePreference() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 3000")
}
}
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
@ -72,8 +60,6 @@ class TabbedBrowsingTest {
@Test @Test
fun openNewTabTest() { fun openNewTabTest() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar { navigationToolbar {
@ -81,26 +67,26 @@ class TabbedBrowsingTest {
mDevice.waitForIdle() mDevice.waitForIdle()
verifyTabCounter("1") verifyTabCounter("1")
}.openTabDrawer { }.openTabDrawer {
verifyExistingTabList() verifyNormalModeSelected()
}.openTabsListThreeDotMenu { verifyExistingOpenTabs("Test_Page_1")
verifyCloseAllTabsButton() closeTab()
verifyShareTabButton() }.openTabDrawer {
verifySelectTabs() verifyNoTabsOpened()
}.openNewTab {
}.submitQuery(defaultWebPage.url.toString()) {
mDevice.waitForIdle()
verifyTabCounter("1")
}.openTabDrawer {
verifyNormalModeSelected()
verifyExistingOpenTabs("Test_Page_1")
} }
} }
@Test @Test
fun openNewPrivateTabTest() { fun openNewPrivateTabTest() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen { }.togglePrivateBrowsingMode() homeScreen {}.togglePrivateBrowsingMode()
homeScreen {
verifyPrivateSessionMessage()
verifyTabButton()
}
navigationToolbar { navigationToolbar {
}.openNewTabAndEnterToBrowser(defaultWebPage.url) { }.openNewTabAndEnterToBrowser(defaultWebPage.url) {
@ -108,7 +94,7 @@ class TabbedBrowsingTest {
verifyTabCounter("1") verifyTabCounter("1")
}.openTabDrawer { }.openTabDrawer {
verifyExistingTabList() verifyExistingTabList()
verifyCloseTabsButton("Test_Page_1") verifyPrivateModeSelected()
}.toggleToNormalTabs { }.toggleToNormalTabs {
verifyNoTabsOpened() verifyNoTabsOpened()
}.toggleToPrivateTabs { }.toggleToPrivateTabs {
@ -156,7 +142,6 @@ class TabbedBrowsingTest {
}.openNewTabAndEnterToBrowser(genericURL.url) { }.openNewTabAndEnterToBrowser(genericURL.url) {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
closeTabViaXButton("Test_Page_1") closeTabViaXButton("Test_Page_1")
verifySnackBarText("Tab closed") verifySnackBarText("Tab closed")
snackBarButtonClick("UNDO") snackBarButtonClick("UNDO")
@ -187,8 +172,7 @@ class TabbedBrowsingTest {
browserScreen { browserScreen {
}.openTabDrawer { }.openTabDrawer {
verifyExistingOpenTabs("Test_Page_1") verifyExistingOpenTabs("Test_Page_1")
}.openNewTab { }.closeTabDrawer { }
}.dismissSearchBar { }
} }
@Test @Test
@ -249,15 +233,12 @@ class TabbedBrowsingTest {
notificationShade { notificationShade {
verifyPrivateTabsNotification() verifyPrivateTabsNotification()
}.clickClosePrivateTabsNotification { }.clickClosePrivateTabsNotification {
// Tap an empty spot on the app homescreen to make sure it's into focus
sendSingleTapToScreen(20, 20)
verifyHomeScreen() verifyHomeScreen()
} }
} }
@Test @Test
fun verifyTabTrayNotShowingStateHalfExpanded() { fun verifyTabTrayNotShowingStateHalfExpanded() {
homeScreen { }.dismissOnboarding()
navigationToolbar { navigationToolbar {
}.openTabTray { }.openTabTray {
@ -282,8 +263,6 @@ class TabbedBrowsingTest {
@Test @Test
fun verifyEmptyTabTray() { fun verifyEmptyTabTray() {
homeScreen { }.dismissOnboarding()
navigationToolbar { navigationToolbar {
}.openTabTray { }.openTabTray {
verifyNoTabsOpened() verifyNoTabsOpened()
@ -298,8 +277,6 @@ class TabbedBrowsingTest {
@Test @Test
fun verifyOpenTabDetails() { fun verifyOpenTabDetails() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar { navigationToolbar {
@ -317,8 +294,6 @@ class TabbedBrowsingTest {
@Test @Test
fun verifyContextMenuShortcuts() { fun verifyContextMenuShortcuts() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar { navigationToolbar {

@ -12,6 +12,7 @@ import org.junit.Before
import org.junit.BeforeClass import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
@ -54,40 +55,68 @@ class ThreeDotMenuMainTest {
@Test @Test
fun threeDotMenuItemsTest() { fun threeDotMenuItemsTest() {
homeScreen { if (FeatureFlags.toolbarMenuFeature) {
}.openThreeDotMenu { homeScreen {
verifySettingsButton() }.openThreeDotMenu {
verifyBookmarksButton() }.openHistory {
verifyHistoryButton() verifyHistoryMenuView()
verifyHelpButton() }.goBackToBrowser {}
verifyWhatsNewButton()
}.openSettings { homeScreen {
verifySettingsView() }.openThreeDotMenu {
}.goBack { }.openBookmarks {
}.openThreeDotMenu { verifyBookmarksMenuView()
}.openHelp { }.closeMenu {}
verifyHelpUrl()
}.openTabDrawer { homeScreen {
}.openNewTab { }.openThreeDotMenu {
}.dismissSearchBar { verifySettingsButton()
}.openThreeDotMenu { verifyBookmarksButton()
}.openWhatsNew { verifyHistoryButton()
verifyWhatsNewURL() }.openSettings {
}.openTabDrawer { verifySettingsView()
}.openNewTab { }.goBack {
}.dismissSearchBar { } }.openThreeDotMenu {
}.goBack {}
homeScreen { } else {
}.openThreeDotMenu { homeScreen {
}.openBookmarks { }.openThreeDotMenu {
verifyBookmarksMenuView() verifySettingsButton()
}.closeMenu { verifyBookmarksButton()
} verifyHistoryButton()
verifyHelpButton()
verifyWhatsNewButton()
}.openSettings {
verifySettingsView()
}.goBack {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.openTabDrawer {
closeTab()
}
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openWhatsNew {
verifyHistoryMenuView() verifyWhatsNewURL()
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
}.closeMenu {
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}
} }
} }
} }

@ -105,6 +105,10 @@ class BookmarksRobot {
fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Cant edit default folders") fun verifySelectDefaultFolderSnackBarText() = assertSnackBarText("Cant edit default folders")
fun verifyCurrentFolderTitle(title: String) { fun verifyCurrentFolderTitle(title: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(title))
.waitForExists(waitingTime)
onView( onView(
allOf( allOf(
withText(title), withText(title),
@ -114,6 +118,14 @@ class BookmarksRobot {
.check(matches(isDisplayed())) .check(matches(isDisplayed()))
} }
fun waitForBookmarksFolderContentToExist(parentFolderName: String, childFolderName: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/navigationToolbar")
.textContains(parentFolderName))
.waitForExists(waitingTime)
mDevice.waitNotNull(Until.findObject(By.text(childFolderName)), waitingTime)
}
fun verifySignInToSyncButton() = fun verifySignInToSyncButton() =
signInToSyncButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) signInToSyncButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

@ -32,6 +32,7 @@ import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import mozilla.components.browser.state.selector.selectedTab
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.Matchers.not import org.hamcrest.Matchers.not
@ -49,8 +50,8 @@ class BrowserRobot {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
fun verifyCurrentPrivateSession(context: Context) { fun verifyCurrentPrivateSession(context: Context) {
val session = context.components.core.sessionManager.selectedSession val selectedTab = context.components.core.store.state.selectedTab
assertTrue("Current session is private", session?.private!!) assertTrue("Current session is private", selectedTab?.content?.private ?: false)
} }
fun verifyUrl(url: String) { fun verifyUrl(url: String) {
@ -465,6 +466,20 @@ class BrowserRobot {
HomeScreenRobot().interact() HomeScreenRobot().interact()
return HomeScreenRobot.Transition() return HomeScreenRobot.Transition()
} }
fun clickTabCrashedCloseButton(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/closeTabButton"))
.waitForExists(waitingTime)
)
val tabCrashedCloseButton = mDevice.findObject(text("Close tab"))
tabCrashedCloseButton.click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
} }
} }
@ -525,3 +540,14 @@ private fun mediaPlayerPlayButton() =
.className("android.widget.Button") .className("android.widget.Button")
.text("Play") .text("Play")
) )
fun clickTabCrashedRestoreButton() {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/restoreTabButton"))
.waitForExists(waitingTime)
)
val tabCrashRestoreButton = mDevice.findObject(UiSelector().resourceIdMatches("$packageName:id/restoreTabButton"))
tabCrashRestoreButton.click()
}

@ -8,13 +8,20 @@ package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.containsString
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
@ -37,6 +44,14 @@ class EnhancedTrackingProtectionRobot {
fun verifyEnhancedTrackingProtectionDetailsStatus(status: String) = fun verifyEnhancedTrackingProtectionDetailsStatus(status: String) =
assertEnhancedTrackingProtectionDetailsStatus(status) assertEnhancedTrackingProtectionDetailsStatus(status)
fun verifyTrackingCookiesBlocked() = assertTrackingCookiesBlocked()
fun verifyFingerprintersBlocked() = assertFingerprintersBlocked()
fun verifyCryptominersBlocked() = assertCryptominersBlocked()
fun verifyBasicLevelTrackingContentBlocked() = assertBasicLevelTrackingContentBlocked()
class Transition { class Transition {
fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition { fun openEnhancedTrackingProtectionSheet(interact: EnhancedTrackingProtectionRobot.() -> Unit): Transition {
openEnhancedTrackingProtectionSheet().click() openEnhancedTrackingProtectionSheet().click()
@ -129,3 +144,45 @@ private fun openEnhancedTrackingProtectionSettings() =
private fun openEnhancedTrackingProtectionDetails() = private fun openEnhancedTrackingProtectionDetails() =
onView(ViewMatchers.withId(R.id.tracking_content)) onView(ViewMatchers.withId(R.id.tracking_content))
private fun assertTrackingCookiesBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/cross_site_tracking"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.cross_site_tracking)).check(matches(isDisplayed()))
}
private fun assertFingerprintersBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/fingerprinters"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.fingerprinters)).check(matches(isDisplayed()))
}
private fun assertCryptominersBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/cryptominers"))
.waitForExists(waitingTime)
onView(withId(R.id.blocking_header)).check(matches(isDisplayed()))
onView(withId(R.id.cryptominers)).check(matches(isDisplayed()))
}
private fun assertBasicLevelTrackingContentBlocked() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/tracking_content"))
.waitForExists(waitingTime)
onView(withId(R.id.tracking_content))
.check(matches(isDisplayed()))
.click()
onView(withId(R.id.blocking_text_list))
.check(
matches(
withText(
containsString(
"social-track-digest256.dummytracker.org\n" +
"ads-track-digest256.dummytracker.org\n" +
"analytics-track-digest256.dummytracker.org"
)
)
)
)
}

@ -309,6 +309,7 @@ class HomeScreenRobot {
} }
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/menuButton")), waitingTime)
threeDotButton().perform(click()) threeDotButton().perform(click())
ThreeDotMenuMainRobot().interact() ThreeDotMenuMainRobot().interact()
@ -485,8 +486,11 @@ private fun assertFocusedNavigationToolbar() =
onView(allOf(withHint("Search or enter address"))) onView(allOf(withHint("Search or enter address")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomeScreen() = onView(ViewMatchers.withResourceName("homeLayout")) private fun assertHomeScreen() {
mDevice.findObject(UiSelector().resourceId("$packageName:id/homeLayout")).waitForExists(waitingTime)
onView(ViewMatchers.withResourceName("homeLayout"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton")) private fun assertHomeMenu() = onView(ViewMatchers.withResourceName("menuButton"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -30,6 +30,7 @@ import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
@ -121,6 +122,30 @@ class NavigationToolbarRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun openTabCrashReporter(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
val crashUrl = "about:crashcontent"
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/toolbar")),
waitingTime
)
urlBar().click()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_edit_url_view")),
waitingTime
)
awesomeBar().perform(replaceText(crashUrl), pressImeActionButton())
runWithIdleRes(sessionLoadedIdlingResource) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/crash_tab_image"))
}
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openThreeDotMenu(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_menu")), waitingTime) mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_menu")), waitingTime)
threeDotButton().click() threeDotButton().click()

@ -67,7 +67,7 @@ class NotificationRobot {
fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition { fun clickClosePrivateTabsNotification(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
NotificationRobot().verifySystemNotificationExists("Close private tabs") NotificationRobot().verifySystemNotificationExists("Close private tabs")
closePrivateTabsNotification().clickAndWaitForNewWindow() closePrivateTabsNotification().click()
HomeScreenRobot().interact() HomeScreenRobot().interact()
return HomeScreenRobot.Transition() return HomeScreenRobot.Transition()

@ -270,6 +270,9 @@ private fun assertSearchEngineList() {
} }
private fun assertEngineListShortcutContains(searchEngineName: String) { private fun assertEngineListShortcutContains(searchEngineName: String) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/awesome_bar"))
.waitForExists(waitingTime)
onView(withId(R.id.awesome_bar)) onView(withId(R.id.awesome_bar))
.perform(swipeDown()) .perform(swipeDown())
.check(matches(hasDescendant(withText(searchEngineName)))) .check(matches(hasDescendant(withText(searchEngineName))))

@ -13,6 +13,7 @@ import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.hasSibling import androidx.test.espresso.matcher.ViewMatchers.hasSibling
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withChild
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
@ -25,6 +26,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
import org.mozilla.fenix.helpers.assertIsChecked import org.mozilla.fenix.helpers.assertIsChecked
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.isChecked import org.mozilla.fenix.helpers.isChecked
@ -49,9 +51,9 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
fun verifyEnhancedTrackingProtectionOptionsGrayedOut() = assertEnhancedTrackingProtectionOptionsGrayedOut() fun verifyEnhancedTrackingProtectionOptionsGrayedOut() = assertEnhancedTrackingProtectionOptionsGrayedOut()
fun verifyEnhancedTrackingProtectionDefaults() = assertEnhancedTrackingProtectionDefaults() fun verifyTrackingProtectionSwitchEnabled() = assertTrackingProtectionSwitchEnabled()
fun clickEnhancedTrackingProtectionDefaults() = onView(withResourceName("switch_widget")).click() fun switchEnhancedTrackingProtectionToggle() = onView(withResourceName("switch_widget")).click()
fun verifyRadioButtonDefaults() = assertRadioButtonDefaults() fun verifyRadioButtonDefaults() = assertRadioButtonDefaults()
@ -60,11 +62,15 @@ class SettingsSubMenuEnhancedTrackingProtectionRobot {
verifyEnhancedTrackingProtectionHeaderDescription() verifyEnhancedTrackingProtectionHeaderDescription()
verifyLearnMoreText() verifyLearnMoreText()
verifyEnhancedTrackingProtectionTextWithSwitchWidget() verifyEnhancedTrackingProtectionTextWithSwitchWidget()
verifyEnhancedTrackingProtectionDefaults() verifyTrackingProtectionSwitchEnabled()
verifyRadioButtonDefaults() verifyRadioButtonDefaults()
verifyEnhancedTrackingProtectionOptions() verifyEnhancedTrackingProtectionOptions()
} }
fun verifyCustomTrackingProtectionSettings() = assertCustomTrackingProtectionSettings()
fun selectTrackingProtectionOption(option: String) = onView(withText(option)).click()
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!! val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
@ -183,7 +189,7 @@ private fun assertEnhancedTrackingProtectionOptionsGrayedOut() {
.check(matches(not(isEnabled(true)))) .check(matches(not(isEnabled(true))))
} }
private fun assertEnhancedTrackingProtectionDefaults() { private fun assertTrackingProtectionSwitchEnabled() {
onView(withResourceName("switch_widget")).check( onView(withResourceName("switch_widget")).check(
matches( matches(
isChecked( isChecked(
@ -218,3 +224,28 @@ private fun goBackButton() =
private fun openExceptions() = private fun openExceptions() =
onView(allOf(withText("Exceptions"))) onView(allOf(withText("Exceptions")))
private fun assertCustomTrackingProtectionSettings() {
scrollToElementByText("Redirect Trackers")
cookiesCheckbox().check(matches(isDisplayed()))
cookiesDropDownMenuDefault().check(matches(isDisplayed()))
trackingContentCheckbox().check(matches(isDisplayed()))
trackingcontentDropDownDefault().check(matches(isDisplayed()))
cryptominersCheckbox().check(matches(isDisplayed()))
fingerprintersCheckbox().check(matches(isDisplayed()))
redirectTrackersCheckbox().check(matches(isDisplayed()))
}
private fun cookiesCheckbox() = onView(withText("Cookies"))
private fun cookiesDropDownMenuDefault() = onView(withText("All cookies (will cause websites to break)"))
private fun trackingContentCheckbox() = onView(withText("Tracking content"))
private fun trackingcontentDropDownDefault() = onView(withText("In all tabs"))
private fun cryptominersCheckbox() = onView(withText("Cryptominers"))
private fun fingerprintersCheckbox() = onView(withText("Fingerprinters"))
private fun redirectTrackersCheckbox() = onView(withText("Redirect Trackers"))

@ -46,6 +46,8 @@ class SettingsSubMenuPrivateBrowsingRobot {
fun verifyPrivateBrowsingShortcutIcon() = assertPrivateBrowsingShortcutIcon() fun verifyPrivateBrowsingShortcutIcon() = assertPrivateBrowsingShortcutIcon()
fun clickPrivateModeScreenshotsSwitch() = screenshotsInPrivateModeSwitch().click()
fun clickOpenLinksInPrivateTabSwitch() = openLinksInPrivateTabSwitch().click() fun clickOpenLinksInPrivateTabSwitch() = openLinksInPrivateTabSwitch().click()
fun addPrivateShortcutToHomescreen() { fun addPrivateShortcutToHomescreen() {
@ -92,6 +94,9 @@ private fun assertNavigationToolBarHeader() {
private fun openLinksInPrivateTabSwitch() = private fun openLinksInPrivateTabSwitch() =
onView(withText("Open links in a private tab")) onView(withText("Open links in a private tab"))
private fun screenshotsInPrivateModeSwitch() =
onView(withText("Allow screenshots in private browsing"))
private fun addPrivateBrowsingShortcutButton() = onView(withText("Add private browsing shortcut")) private fun addPrivateBrowsingShortcutButton() = onView(withText("Add private browsing shortcut"))
private fun goBackButton() = onView(withContentDescription("Navigate up")) private fun goBackButton() = onView(withContentDescription("Navigate up"))

@ -8,25 +8,20 @@ package org.mozilla.fenix.ui.robots
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withChild import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -67,33 +62,6 @@ class SettingsSubMenuSearchRobot {
saveNewSearchEngine() saveNewSearchEngine()
} }
fun selectAddCustomSearchEngine() = onView(withText("Other")).click()
fun typeCustomEngineDetails(engineName: String, engineURL: String) {
onView(withId(R.id.edit_engine_name))
.perform(clearText())
.perform(typeText(engineName))
onView(withId(R.id.edit_search_string))
.perform(clearText())
.perform(typeText(engineURL))
}
fun openEngineOverflowMenu(searchEngineName: String) {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/overflow_menu")
).waitForExists(waitingTime)
threeDotMenu(searchEngineName).click()
}
fun clickEdit() = onView(withText("Edit")).click()
fun saveEditSearchEngine() {
onView(withId(R.id.save_button)).click()
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/recycler_view")
).waitForExists(waitingTime)
}
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -220,11 +188,3 @@ private fun addSearchEngineSaveButton() = onView(withId(R.id.add_search_engine))
private fun assertEngineListContains(searchEngineName: String) { private fun assertEngineListContains(searchEngineName: String) {
onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName)))) onView(withId(R.id.search_engine_group)).check(matches(hasDescendant(withText(searchEngineName))))
} }
private fun threeDotMenu(searchEngineName: String) =
onView(
allOf(
withId(R.id.overflow_menu),
withParent(withChild(withText(searchEngineName)))
)
)

@ -15,6 +15,7 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.NoMatchingViewException import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.UiController import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.GeneralLocation
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
@ -42,7 +43,9 @@ import org.hamcrest.Matcher
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.clickAtLocationInView
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource import org.mozilla.fenix.helpers.idlingresource.BottomSheetBehaviorStateIdlingResource
import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorHalfExpandedMaxRatioMatcher
@ -85,23 +88,46 @@ class TabDrawerRobot {
mDevice.findObject( mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close") UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
).waitForExists(waitingTime) ).waitForExists(waitingTime)
closeTabButton().click()
var retries = 0 // number of retries before failing, will stop at 2
do {
closeTabButton().click()
retries++
} while (mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
).exists() && retries < 3
)
} }
fun swipeTabRight(title: String) = fun swipeTabRight(title: String) {
tab(title).perform(ViewActions.swipeRight()) var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) {
tab(title).perform(ViewActions.swipeRight())
retries++
}
}
fun swipeTabLeft(title: String) = fun swipeTabLeft(title: String) {
tab(title).perform(ViewActions.swipeLeft()) var retries = 0 // number of retries before failing, will stop at 2
while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3) {
tab(title).perform(ViewActions.swipeLeft())
retries++
}
}
fun closeTabViaXButton(title: String) { fun closeTabViaXButton(title: String) {
val closeButton = onView( mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
allOf( var retries = 0 // number of retries before failing, will stop at 2
withId(R.id.mozac_browser_tabstray_close), do {
withContentDescription("Close tab $title") val closeButton = onView(
allOf(
withId(R.id.mozac_browser_tabstray_close),
withContentDescription("Close tab $title")
)
) )
) closeButton.perform(click())
closeButton.perform(click()) retries++
} while (mDevice.findObject(UiSelector().text(title)).exists() && retries < 3)
} }
fun verifySnackBarText(expectedText: String) { fun verifySnackBarText(expectedText: String) {
@ -231,7 +257,9 @@ class TabDrawerRobot {
} }
fun clickTopBar(interact: TabDrawerRobot.() -> Unit): Transition { fun clickTopBar(interact: TabDrawerRobot.() -> Unit): Transition {
onView(withId(R.id.topBar)).click() // The topBar contains other views.
// Don't do the default click in the middle, rather click in some free space - top right.
onView(withId(R.id.topBar)).clickAtLocationInView(GeneralLocation.TOP_RIGHT)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return Transition() return Transition()
} }
@ -256,8 +284,11 @@ class TabDrawerRobot {
} }
fun waitForTabTrayBehaviorToIdle(interact: TabDrawerRobot.() -> Unit): Transition { fun waitForTabTrayBehaviorToIdle(interact: TabDrawerRobot.() -> Unit): Transition {
// Need to get the behavior of tab_wrapper and wait for that to idle.
var behavior: BottomSheetBehavior<*>? = null var behavior: BottomSheetBehavior<*>? = null
onView(withId(R.id.tab_wrapper)).perform(object : ViewAction {
// Null check here since it's possible that the view is already animated away from the screen.
onView(withId(R.id.tab_wrapper))?.perform(object : ViewAction {
override fun getDescription(): String { override fun getDescription(): String {
return "Postpone actions to after the BottomSheetBehavior has settled" return "Postpone actions to after the BottomSheetBehavior has settled"
} }
@ -270,9 +301,13 @@ class TabDrawerRobot {
behavior = BottomSheetBehavior.from(view!!) behavior = BottomSheetBehavior.from(view!!)
} }
}) })
runWithIdleRes(BottomSheetBehaviorStateIdlingResource(behavior!!)) {
TabDrawerRobot().interact() behavior?.let {
runWithIdleRes(BottomSheetBehaviorStateIdlingResource(it)) {
TabDrawerRobot().interact()
}
} }
return Transition() return Transition()
} }
@ -326,6 +361,11 @@ private fun threeDotMenu() = onView(withId(R.id.tab_tray_overflow))
private fun assertExistingOpenTabs(title: String) { private fun assertExistingOpenTabs(title: String) {
try { try {
mDevice.findObject(UiSelector()
.resourceId("$packageName:id/mozac_browser_tabstray_title")
.textContains(title))
.waitForExists(waitingTime)
tab(title).check(matches(isDisplayed())) tab(title).check(matches(isDisplayed()))
} catch (e: NoMatchingViewException) { } catch (e: NoMatchingViewException) {
onView(withId(R.id.tabsTray)).perform( onView(withId(R.id.tabsTray)).perform(

@ -38,6 +38,7 @@ import androidx.test.uiautomator.Until
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -124,21 +125,33 @@ class ThreeDotMenuMainRobot {
fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifyThreeDotMainMenuItems() { fun verifyThreeDotMainMenuItems() {
verifyAddOnsButton() if (FeatureFlags.toolbarMenuFeature) {
verifyDownloadsButton() verifyDownloadsButton()
verifyHistoryButton() verifyHistoryButton()
verifyBookmarksButton() verifyBookmarksButton()
verifySyncedTabsButton() verifySettingsButton()
verifySettingsButton() verifyDesktopSite()
verifyFindInPageButton() verifySaveCollection()
verifyAddFirefoxHome() verifyShareButton()
verifyAddToMobileHome() verifyForwardButton()
verifyDesktopSite() verifyRefreshButton()
verifySaveCollection() } else {
verifyAddBookmarkButton() verifyAddOnsButton()
verifyShareButton() verifyDownloadsButton()
verifyForwardButton() verifyHistoryButton()
verifyRefreshButton() verifyBookmarksButton()
verifySyncedTabsButton()
verifySettingsButton()
verifyFindInPageButton()
verifyAddFirefoxHome()
verifyAddToMobileHome()
verifyDesktopSite()
verifySaveCollection()
verifyAddBookmarkButton()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
}
} }
private fun assertShareTabsOverlay() { private fun assertShareTabsOverlay() {
@ -390,7 +403,8 @@ private fun assertSettingsButton() = settingsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isCompletelyDisplayed())) .check(matches(isCompletelyDisplayed()))
private fun addOnsButton() = onView(allOf(withText("Add-ons"))) private val addOnsText = if (FeatureFlags.toolbarMenuFeature) "Extensions" else "Add-ons"
private fun addOnsButton() = onView(allOf(withText(addOnsText)))
private fun assertAddOnsButton() { private fun assertAddOnsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

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

@ -23,7 +23,7 @@
android:allowBackup="false" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="${requestLegacyExternalStorage}"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/NormalTheme" android:theme="@style/NormalTheme"
@ -119,6 +119,9 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".home.mozonline.PrivacyContentDisplayActivity"
android:exported="false"/>
<activity <activity
android:name=".customtabs.ExternalAppBrowserActivity" android:name=".customtabs.ExternalAppBrowserActivity"
android:autoRemoveFromRecents="false" android:autoRemoveFromRecents="false"
@ -229,9 +232,6 @@
<activity android:name=".settings.account.AuthIntentReceiverActivity" <activity android:name=".settings.account.AuthIntentReceiverActivity"
android:exported="false" /> android:exported="false" />
<service android:name=".media.MediaService"
android:exported="false" />
<service android:name=".media.MediaSessionService" <service android:name=".media.MediaSessionService"
android:exported="false" /> android:exported="false" />

@ -15,33 +15,17 @@ object FeatureFlags {
const val pullToRefreshEnabled = true const val pullToRefreshEnabled = true
/** /**
* Shows Synced Tabs in the tabs tray. * Enables the Nimbus experiments library.
*
* Tracking issue: https://github.com/mozilla-mobile/fenix/issues/13892
*/ */
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug val nimbusExperiments = Config.channel.isNightlyOrDebug
/** /**
* Enables the Nimbus experiments library, especially the settings toggle to opt-out of * Enables WebAuthn support.
* all experiments.
*/ */
// IMPORTANT: Only turn this back on once the following issues are resolved: val webAuthFeature = Config.channel.isNightlyOrDebug
// - https://github.com/mozilla-mobile/fenix/issues/17086: Calls to
// getExperimentBranch seem to block on updateExperiments causing a
// large performance regression loading the home screen.
// - https://github.com/mozilla-mobile/fenix/issues/17143: Despite
// having wrapped getExperimentBranch/withExperiments in a catch-all
// users are still experiencing crashes.
const val nimbusExperiments = false
/**
* Enables the new MediaSession API.
*/
@Suppress("MayBeConst")
val newMediaSessionApi = true
/** /**
* Enables experimental WebAuthn support. This implementation should never reach release! * Shows new three-dot toolbar menu design.
*/ */
val webAuthFeature = Config.channel.isNightlyOrDebug val toolbarMenuFeature = Config.channel.isDebug
} }

@ -20,7 +20,6 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
@ -30,6 +29,7 @@ import mozilla.components.lib.crash.CrashReporter
import mozilla.components.service.glean.Glean import mozilla.components.service.glean.Glean
import mozilla.components.service.glean.config.Configuration import mozilla.components.service.glean.config.Configuration
import mozilla.components.service.glean.net.ConceptFetchHttpUploader import mozilla.components.service.glean.net.ConceptFetchHttpUploader
import mozilla.components.support.base.facts.register
import mozilla.components.support.base.log.Log import mozilla.components.support.base.log.Log
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.ktx.android.content.isMainProcess import mozilla.components.support.ktx.android.content.isMainProcess
@ -39,9 +39,12 @@ import mozilla.components.support.rusthttp.RustHttpConfig
import mozilla.components.support.rustlog.RustLog import mozilla.components.support.rustlog.RustLog
import mozilla.components.support.utils.logElapsedTime import mozilla.components.support.utils.logElapsedTime
import mozilla.components.support.webextensions.WebExtensionSupport import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.components.metrics.SecurePrefsTelemetry
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.ProfilerMarkerFactProcessor
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StorageStatsMetrics import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.runBlockingIncrement import org.mozilla.fenix.perf.runBlockingIncrement
@ -70,6 +73,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
private set private set
override fun onCreate() { override fun onCreate() {
val methodDurationTimerId = PerfStartup.applicationOnCreate.start() // DO NOT MOVE ANYTHING ABOVE HERE.
super.onCreate() super.onCreate()
setupInAllProcesses() setupInAllProcesses()
@ -91,6 +95,9 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
} }
setupInMainProcessOnly() setupInMainProcessOnly()
// We use start/stop instead of measure so we don't measure outside the main process.
PerfStartup.applicationOnCreate.stopAndAccumulate(methodDurationTimerId) // DO NOT MOVE ANYTHING BELOW HERE.
} }
protected open fun initializeGlean() { protected open fun initializeGlean() {
@ -119,6 +126,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
@CallSuper @CallSuper
open fun setupInMainProcessOnly() { open fun setupInMainProcessOnly() {
ProfilerMarkerFactProcessor.create { components.core.engine.profiler }.register()
run { run {
// Attention: Do not invoke any code from a-s in this scope. // Attention: Do not invoke any code from a-s in this scope.
val megazordSetup = setupMegazord() val megazordSetup = setupMegazord()
@ -195,6 +204,8 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.core.bookmarksStorage.warmUp() components.core.bookmarksStorage.warmUp()
components.core.passwordsStorage.warmUp() components.core.passwordsStorage.warmUp()
} }
SecurePrefsTelemetry(this@FenixApplication, components.analytics.experiments).startTests()
} }
// Account manager initialization needs to happen on the main thread. // Account manager initialization needs to happen on the main thread.
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
@ -307,6 +318,10 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// ... but RustHttpConfig.setClient() and RustLog.enable() can be called later. // ... but RustHttpConfig.setClient() and RustLog.enable() can be called later.
RustHttpConfig.setClient(lazy { components.core.client }) RustHttpConfig.setClient(lazy { components.core.client })
RustLog.enable(components.analytics.crashReporter) RustLog.enable(components.analytics.crashReporter)
// We want to ensure Nimbus is initialized as early as possible so we can
// experiment on features close to startup.
// But we need viaduct (the RustHttp client) to be ready before we do.
components.analytics.experiments.initialize()
} }
} }
@ -417,9 +432,19 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.core.store.state.selectedTab?.content?.private components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab ?: components.settings.openLinksInAPrivateTab
val session = Session(url, shouldCreatePrivateSession) if (shouldCreatePrivateSession) {
components.core.sessionManager.add(session, true, engineSession) components.useCases.tabsUseCases.addPrivateTab(
session.id url = url,
selectTab = true,
engineSession = engineSession
)
} else {
components.useCases.tabsUseCases.addTab(
url = url,
selectTab = true,
engineSession = engineSession
)
}
}, },
onCloseTabOverride = { onCloseTabOverride = {
_, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId) _, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)

@ -17,7 +17,7 @@ import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.WindowManager import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -52,6 +52,7 @@ import mozilla.components.feature.privatemode.notification.PrivateNotificationFe
import mozilla.components.feature.search.BrowserStoreSearchAdapter import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.feature.search.ext.legacy import mozilla.components.feature.search.ext.legacy
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.ktx.android.arch.lifecycle.addObservers import mozilla.components.support.ktx.android.arch.lifecycle.addObservers
import mozilla.components.support.ktx.android.content.call import mozilla.components.support.ktx.android.content.call
@ -64,6 +65,7 @@ import mozilla.components.support.utils.SafeIntent
import mozilla.components.support.utils.toSafeIntent import mozilla.components.support.utils.toSafeIntent
import mozilla.components.support.webextensions.WebExtensionPopupFeature import mozilla.components.support.webextensions.WebExtensionPopupFeature
import org.mozilla.fenix.GleanMetrics.Metrics import org.mozilla.fenix.GleanMetrics.Metrics
import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.addons.AddonDetailsFragmentDirections import org.mozilla.fenix.addons.AddonDetailsFragmentDirections
import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections import org.mozilla.fenix.addons.AddonPermissionsDetailsFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -91,6 +93,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.session.PrivateNotificationService
@ -161,7 +164,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private lateinit var navigationToolbar: Toolbar private lateinit var navigationToolbar: Toolbar
final override fun onCreate(savedInstanceState: Bundle?) { final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measure {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity")
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager) components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
// There is disk read violations on some devices such as samsung and pixel for android 9/10 // There is disk read violations on some devices such as samsung and pixel for android 9/10
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
@ -277,6 +283,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Even if screenshots are allowed, we hide private content in the recents screen in onPause
// so onResume we should go back to setting these flags with the user screenshot setting
// See https://github.com/mozilla-mobile/fenix/issues/11153
updateSecureWindowFlags(settings().lastKnownMode)
// Diagnostic breadcrumb for "Display already aquired" crash: // Diagnostic breadcrumb for "Display already aquired" crash:
// https://github.com/mozilla-mobile/android-components/issues/7960 // https://github.com/mozilla-mobile/android-components/issues/7960
breadcrumb( breadcrumb(
@ -318,6 +329,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
breadcrumb( breadcrumb(
message = "onStart()" message = "onStart()"
) )
ProfilerMarkers.homeActivityOnStart(rootContainer, components.core.engine.profiler)
} }
override fun onStop() { override fun onStop() {
@ -340,8 +353,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
settings().shouldReturnToBrowser = settings().shouldReturnToBrowser =
components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty() components.core.store.state.getNormalOrPrivateTabs(private = false).isNotEmpty()
// Even if screenshots are allowed, we want to hide private content in the recents screen
// See https://github.com/mozilla-mobile/fenix/issues/11153
if (settings().lastKnownMode.isPrivate) { if (settings().lastKnownMode.isPrivate) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) window.addFlags(FLAG_SECURE)
} }
// We will remove this when AC code lands to emit a fact on getTopSites in DefaultTopSitesStorage // We will remove this when AC code lands to emit a fact on getTopSites in DefaultTopSitesStorage
@ -536,6 +551,15 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
super.onBackPressed() super.onBackPressed()
} }
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
supportFragmentManager.primaryNavigationFragment?.childFragmentManager?.fragments?.forEach {
if (it is ActivityResultHandler && it.onActivityResult(requestCode, data, resultCode)) {
return
}
}
super.onActivityResult(requestCode, resultCode, data)
}
private fun shouldUseCustomBackLongPress(): Boolean { private fun shouldUseCustomBackLongPress(): Boolean {
val isAndroidN = val isAndroidN =
Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1 Build.VERSION.SDK_INT == Build.VERSION_CODES.N || Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1
@ -840,7 +864,18 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager { protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
return DefaultBrowsingModeManager(initialMode, components.settings) { newMode -> return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
updateSecureWindowFlags(newMode)
themeManager.currentTheme = newMode themeManager.currentTheme = newMode
}.also {
updateSecureWindowFlags(initialMode)
}
}
fun updateSecureWindowFlags(mode: BrowsingMode = browsingModeManager.mode) {
if (mode == BrowsingMode.Private && !settings().allowScreenshotsInPrivateMode) {
window.addFlags(FLAG_SECURE)
} else {
window.clearFlags(FLAG_SECURE)
} }
} }

@ -26,6 +26,9 @@ class IntentReceiverActivity : Activity() {
@VisibleForTesting @VisibleForTesting
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "IntentReceiverActivity")
// StrictMode violation on certain devices such as Samsung // StrictMode violation on certain devices such as Samsung
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

@ -8,17 +8,23 @@ import androidx.annotation.VisibleForTesting
import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.DownloadAction import mozilla.components.browser.state.action.DownloadAction
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.EngineState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.support.base.android.Clock
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.GleanMetrics.Engine as EngineMetrics
/** /**
* [Middleware] to record telemetry in response to [BrowserAction]s. * [Middleware] to record telemetry in response to [BrowserAction]s.
@ -49,7 +55,7 @@ class TelemetryMiddleware(
} }
} }
@Suppress("TooGenericExceptionCaught", "ComplexMethod") @Suppress("TooGenericExceptionCaught", "ComplexMethod", "NestedBlockDepth")
override fun invoke( override fun invoke(
context: MiddlewareContext<BrowserState, BrowserAction>, context: MiddlewareContext<BrowserState, BrowserAction>,
next: (BrowserAction) -> Unit, next: (BrowserAction) -> Unit,
@ -60,8 +66,12 @@ class TelemetryMiddleware(
is ContentAction.UpdateLoadingStateAction -> { is ContentAction.UpdateLoadingStateAction -> {
context.state.findTab(action.sessionId)?.let { tab -> context.state.findTab(action.sessionId)?.let { tab ->
// Record UriOpened event when a non-private page finishes loading // Record UriOpened event when a non-private page finishes loading
if (tab.content.loading && !action.loading && !tab.content.private) { if (tab.content.loading && !action.loading) {
metrics.track(Event.UriOpened) if (!tab.content.private) {
metrics.track(Event.UriOpened)
}
metrics.track(Event.NormalAndPrivateUriOpened)
} }
} }
} }
@ -90,6 +100,10 @@ class TelemetryMiddleware(
is DownloadAction.AddDownloadAction -> { is DownloadAction.AddDownloadAction -> {
metrics.track(Event.DownloadAdded) metrics.track(Event.DownloadAdded)
} }
is EngineAction.KillEngineSessionAction -> {
val tab = context.state.findTabOrCustomTab(action.sessionId)
onEngineSessionKilled(context.state, tab)
}
} }
next(action) next(action)
@ -104,7 +118,44 @@ class TelemetryMiddleware(
is TabListAction.RestoreAction -> { is TabListAction.RestoreAction -> {
// Update/Persist tabs count whenever it changes // Update/Persist tabs count whenever it changes
settings.openTabsCount = context.state.normalTabs.count() settings.openTabsCount = context.state.normalTabs.count()
if (context.state.normalTabs.count() > 0) {
metrics.track(Event.HaveOpenTabs)
} else {
metrics.track(Event.HaveNoOpenTabs)
}
} }
} }
} }
/**
* Collecting some engine-specific (GeckoView) telemetry.
* https://github.com/mozilla-mobile/android-components/issues/9366
*/
private fun onEngineSessionKilled(state: BrowserState, tab: SessionState?) {
if (tab == null) {
logger.debug("Could not find tab for killed engine session")
return
}
val isSelected = tab.id == state.selectedTabId
val ageNanos = tab.engineState.ageNanos()
// Increment the counter of killed foreground/background tabs
val tabKillLabel = if (isSelected) { "foreground" } else { "background" }
EngineMetrics.tabKills[tabKillLabel].add()
// Record the age of the engine session of the killed foreground/background tab.
if (isSelected && ageNanos != null) {
EngineMetrics.killForegroundAge.setRawNanos(ageNanos)
} else if (ageNanos != null) {
EngineMetrics.killBackgroundAge.setRawNanos(ageNanos)
}
}
}
@Suppress("MagicNumber")
private fun EngineState.ageNanos(): Long? {
val timestamp = (timestamp ?: return null)
val now = Clock.elapsedRealtime()
return (now - timestamp) * 1_000_000
} }

@ -38,6 +38,7 @@ import mozilla.components.feature.addons.ui.PermissionsDialogFragment
import mozilla.components.feature.addons.ui.translateName import mozilla.components.feature.addons.ui.translateName
import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment import io.github.forkmaintainers.iceraven.components.PagedAddonInstallationDialogFragment
import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter import io.github.forkmaintainers.iceraven.components.PagedAddonsManagerAdapter
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -357,6 +358,7 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
adapter?.updateAddon(it) adapter?.updateAddon(it)
addonProgressOverlay?.visibility = View.GONE addonProgressOverlay?.visibility = View.GONE
showInstallationDialog(it) showInstallationDialog(it)
Addons.hasInstalledAddons.set(true)
} }
}, },
onError = { _, e -> onError = { _, e ->

@ -21,6 +21,7 @@ import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translateName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -127,6 +128,7 @@ class InstalledAddonDetailsFragment : Fragment() {
) )
) )
} }
Addons.hasEnabledAddons.set(true)
} }
}, },
onError = { onError = {

@ -6,6 +6,7 @@ package org.mozilla.fenix.browser
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
@ -37,13 +38,15 @@ import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.findTabOrCustomTab
import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findTabOrCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
@ -57,6 +60,7 @@ import mozilla.components.feature.contextmenu.ContextMenuCandidate
import mozilla.components.feature.contextmenu.ContextMenuFeature import mozilla.components.feature.contextmenu.ContextMenuFeature
import mozilla.components.feature.downloads.DownloadsFeature import mozilla.components.feature.downloads.DownloadsFeature
import mozilla.components.feature.downloads.manager.FetchDownloadManager import mozilla.components.feature.downloads.manager.FetchDownloadManager
import mozilla.components.feature.downloads.share.ShareDownloadFeature
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature import mozilla.components.feature.media.fullscreen.MediaSessionFullscreenFeature
import mozilla.components.feature.privatemode.feature.SecureWindowFeature import mozilla.components.feature.privatemode.feature.SecureWindowFeature
@ -68,7 +72,6 @@ import mozilla.components.feature.session.FullScreenFeature
import mozilla.components.feature.session.PictureInPictureFeature import mozilla.components.feature.session.PictureInPictureFeature
import mozilla.components.feature.session.SessionFeature import mozilla.components.feature.session.SessionFeature
import mozilla.components.feature.session.SwipeRefreshFeature import mozilla.components.feature.session.SwipeRefreshFeature
import mozilla.components.feature.session.behavior.EngineViewBottomBehavior
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissionsFeature import mozilla.components.feature.sitepermissions.SitePermissionsFeature
import mozilla.components.lib.state.ext.consumeFlow import mozilla.components.lib.state.ext.consumeFlow
@ -81,6 +84,7 @@ import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.IntentReceiverActivity
@ -100,9 +104,7 @@ import org.mozilla.fenix.components.toolbar.BrowserToolbarView
import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor import org.mozilla.fenix.components.toolbar.BrowserToolbarViewInteractor
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarController
import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController import org.mozilla.fenix.components.toolbar.DefaultBrowserToolbarMenuController
import org.mozilla.fenix.components.toolbar.SwipeRefreshScrollingViewBehavior
import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarIntegration
import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.downloads.DynamicDownloadDialog import org.mozilla.fenix.downloads.DynamicDownloadDialog
import org.mozilla.fenix.ext.accessibilityManager import org.mozilla.fenix.ext.accessibilityManager
@ -124,8 +126,10 @@ import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.allowUndo import org.mozilla.fenix.utils.allowUndo
import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
/** /**
* Base fragment extended by [BrowserFragment]. * Base fragment extended by [BrowserFragment].
@ -134,7 +138,7 @@ import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
*/ */
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, ActivityResultHandler,
OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener { OnBackLongPressedListener, AccessibilityManager.AccessibilityStateChangeListener {
private lateinit var browserFragmentStore: BrowserFragmentStore private lateinit var browserFragmentStore: BrowserFragmentStore
@ -157,6 +161,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>() private val sessionFeature = ViewBoundFeatureWrapper<SessionFeature>()
private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>() private val contextMenuFeature = ViewBoundFeatureWrapper<ContextMenuFeature>()
private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>() private val downloadsFeature = ViewBoundFeatureWrapper<DownloadsFeature>()
private val shareDownloadsFeature = ViewBoundFeatureWrapper<ShareDownloadFeature>()
private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>() private val appLinksFeature = ViewBoundFeatureWrapper<AppLinksFeature>()
private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>() private val promptsFeature = ViewBoundFeatureWrapper<PromptFeature>()
private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>() private val findInPageIntegration = ViewBoundFeatureWrapper<FindInPageIntegration>()
@ -168,8 +173,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
private val sitePermissionWifiIntegration = private val sitePermissionWifiIntegration =
ViewBoundFeatureWrapper<SitePermissionsWifiIntegration>() ViewBoundFeatureWrapper<SitePermissionsWifiIntegration>()
private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>() private val secureWindowFeature = ViewBoundFeatureWrapper<SecureWindowFeature>()
private var fullScreenMediaFeature =
ViewBoundFeatureWrapper<MediaFullscreenOrientationFeature>()
private var fullScreenMediaSessionFeature = private var fullScreenMediaSessionFeature =
ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>() ViewBoundFeatureWrapper<MediaSessionFullscreenFeature>()
private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>() private val searchFeature = ViewBoundFeatureWrapper<SearchFeature>()
@ -253,7 +256,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
@CallSuper @CallSuper
internal open fun initializeUI(view: View, tab: SessionState) { internal open fun initializeUI(view: View, tab: SessionState) {
val context = requireContext() val context = requireContext()
val sessionManager = context.components.core.sessionManager
val store = context.components.core.store val store = context.components.core.store
val activity = requireActivity() as HomeActivity val activity = requireActivity() as HomeActivity
@ -280,14 +282,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
) )
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
store = store, store = store,
tabsUseCases = requireComponents.useCases.tabsUseCases,
activity = activity, activity = activity,
navController = findNavController(), navController = findNavController(),
metrics = requireComponents.analytics.metrics, metrics = requireComponents.analytics.metrics,
readerModeController = readerMenuController, readerModeController = readerMenuController,
sessionManager = requireComponents.core.sessionManager,
engineView = engineView, engineView = engineView,
homeViewModel = homeViewModel, homeViewModel = homeViewModel,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSessionId = customTabSessionId,
onTabCounterClicked = { onTabCounterClicked = {
thumbnailsFeature.get()?.requestScreenshot() thumbnailsFeature.get()?.requestScreenshot()
findNavController().nav( findNavController().nav(
@ -317,17 +319,17 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
) )
val browserToolbarMenuController = DefaultBrowserToolbarMenuController( val browserToolbarMenuController = DefaultBrowserToolbarMenuController(
store = store,
activity = activity, activity = activity,
navController = findNavController(), navController = findNavController(),
metrics = requireComponents.analytics.metrics, metrics = requireComponents.analytics.metrics,
settings = context.settings(), settings = context.settings(),
readerModeController = readerMenuController, readerModeController = readerMenuController,
sessionManager = requireComponents.core.sessionManager,
sessionFeature = sessionFeature, sessionFeature = sessionFeature,
findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } },
swipeRefresh = swipeRefresh, swipeRefresh = swipeRefresh,
browserAnimator = browserAnimator, browserAnimator = browserAnimator,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSessionId = customTabSessionId,
openInFenixIntent = openInFenixIntent, openInFenixIntent = openInFenixIntent,
bookmarkTapped = { url: String, title: String -> bookmarkTapped = { url: String, title: String ->
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {
@ -349,7 +351,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
container = view.browserLayout, container = view.browserLayout,
toolbarPosition = context.settings().toolbarPosition, toolbarPosition = context.settings().toolbarPosition,
interactor = browserInteractor, interactor = browserInteractor,
customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, customTabSession = customTabSessionId?.let { store.state.findCustomTab(it) },
lifecycleOwner = viewLifecycleOwner lifecycleOwner = viewLifecycleOwner
) )
@ -405,25 +407,21 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
view = view view = view
) )
if (newMediaSessionApi) { fullScreenMediaSessionFeature.set(
fullScreenMediaSessionFeature.set( feature = MediaSessionFullscreenFeature(
feature = MediaSessionFullscreenFeature( requireActivity(),
requireActivity(), context.components.core.store
context.components.core.store ),
), owner = this,
owner = this, view = view
view = view )
)
} else { val shareDownloadFeature = ShareDownloadFeature(
fullScreenMediaFeature.set( context = context.applicationContext,
feature = MediaFullscreenOrientationFeature( httpClient = context.components.core.client,
requireActivity(), store = store,
context.components.core.store tabId = customTabSessionId
), )
owner = this,
view = view
)
}
val downloadFeature = DownloadsFeature( val downloadFeature = DownloadsFeature(
context.applicationContext, context.applicationContext,
@ -461,9 +459,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus ->
// If the download is just paused, don't show any in-app notification // If the download is just paused, don't show any in-app notification
if (downloadJobStatus == DownloadState.Status.COMPLETED || if (shouldShowCompletedDownloadDialog(downloadState, downloadJobStatus)) {
downloadJobStatus == DownloadState.Status.FAILED
) {
saveDownloadDialogState( saveDownloadDialogState(
downloadState.sessionId, downloadState.sessionId,
@ -491,19 +487,22 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) }
) )
// Don't show the dialog if we aren't in the tab that started the download dynamicDownloadDialog.show()
if (downloadState.sessionId == sessionManager.selectedSession?.id) { browserToolbarView.expand()
dynamicDownloadDialog.show()
browserToolbarView.expand()
}
} }
} }
resumeDownloadDialogState( resumeDownloadDialogState(
sessionManager.selectedSession?.id, getCurrentTab()?.id,
store, view, context, toolbarHeight store, view, context, toolbarHeight
) )
shareDownloadsFeature.set(
shareDownloadFeature,
owner = this,
view = view
)
downloadsFeature.set( downloadsFeature.set(
downloadFeature, downloadFeature,
owner = this, owner = this,
@ -532,7 +531,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
promptsFeature.set( promptsFeature.set(
feature = PromptFeature( feature = PromptFeature(
fragment = this, activity = activity,
store = store, store = store,
customTabId = customTabSessionId, customTabId = customTabSessionId,
fragmentManager = parentFragmentManager, fragmentManager = parentFragmentManager,
@ -553,7 +552,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
val directions = NavGraphDirections.actionGlobalShareFragment( val directions = NavGraphDirections.actionGlobalShareFragment(
data = arrayOf(shareData), data = arrayOf(shareData),
showPage = true, showPage = true,
sessionId = getSessionById()?.id sessionId = getCurrentTab()?.id
) )
findNavController().navigate(directions) findNavController().navigate(directions)
} }
@ -587,14 +586,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
searchFeature.set( searchFeature.set(
feature = SearchFeature(store, customTabSessionId) { request, tabId -> feature = SearchFeature(store, customTabSessionId) { request, tabId ->
val parentSession = sessionManager.findSessionById(tabId) val parentSession = store.state.findTabOrCustomTab(tabId)
val useCase = if (request.isPrivate) { val useCase = if (request.isPrivate) {
requireComponents.useCases.searchUseCases.newPrivateTabSearch requireComponents.useCases.searchUseCases.newPrivateTabSearch
} else { } else {
requireComponents.useCases.searchUseCases.newTabSearch requireComponents.useCases.searchUseCases.newTabSearch
} }
if (parentSession?.isCustomTabSession() == true) { if (parentSession is CustomTabSessionState) {
useCase.invoke(request.query) useCase.invoke(request.query)
requireActivity().startActivity(openInFenixIntent) requireActivity().startActivity(openInFenixIntent)
} else { } else {
@ -643,7 +642,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
view = view view = view
) )
if (FeatureFlags.webAuthFeature) { // This component feature only works on Fenix when built on Mozilla infrastructure.
if (FeatureFlags.webAuthFeature && BuildConfig.MOZILLA_OFFICIAL) {
webAuthnFeature.set( webAuthnFeature.set(
feature = WebAuthnFeature( feature = WebAuthnFeature(
engine = requireComponents.core.engine, engine = requireComponents.core.engine,
@ -818,34 +818,37 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
!inFullScreen !inFullScreen
} }
private fun initializeEngineView(toolbarHeight: Int) { @VisibleForTesting
internal fun initializeEngineView(toolbarHeight: Int) {
val context = requireContext() val context = requireContext()
if (context.settings().isDynamicToolbarEnabled) { if (!context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled) {
engineView.setDynamicToolbarMaxHeight(toolbarHeight) getEngineView().setDynamicToolbarMaxHeight(toolbarHeight)
val behavior = when (context.settings().toolbarPosition) { val toolbarPosition = if (context.settings().shouldUseBottomToolbar) {
// Set engineView dynamic vertical clipping depending on the toolbar position. MozacToolbarPosition.BOTTOM
ToolbarPosition.BOTTOM -> EngineViewBottomBehavior(context, null) } else {
// Set scroll flags depending on if if the browser or the website is doing the scroll. MozacToolbarPosition.TOP
ToolbarPosition.TOP -> SwipeRefreshScrollingViewBehavior( }
(getSwipeRefreshLayout().layoutParams as CoordinatorLayout.LayoutParams).behavior =
EngineViewBrowserToolbarBehavior(
context, context,
null, null,
engineView, getSwipeRefreshLayout(),
browserToolbarView toolbarHeight,
toolbarPosition
) )
}
(swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams).behavior = behavior
} else { } else {
// Ensure webpage's bottom elements are aligned to the very bottom of the engineView. // Ensure webpage's bottom elements are aligned to the very bottom of the engineView.
engineView.setDynamicToolbarMaxHeight(0) getEngineView().setDynamicToolbarMaxHeight(0)
// Effectively place the engineView on top of the toolbar if that is not dynamic. // Effectively place the engineView on top/below of the toolbar if that is not dynamic.
val swipeRefreshParams =
getSwipeRefreshLayout().layoutParams as CoordinatorLayout.LayoutParams
if (context.settings().shouldUseBottomToolbar) { if (context.settings().shouldUseBottomToolbar) {
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams swipeRefreshParams.bottomMargin = toolbarHeight
browserEngine.bottomMargin = } else {
requireContext().resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) swipeRefreshParams.topMargin = toolbarHeight
} }
} }
} }
@ -1009,7 +1012,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
final override fun onViewStateRestored(savedInstanceState: Bundle?) { final override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState) super.onViewStateRestored(savedInstanceState)
savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let { savedInstanceState?.getString(KEY_CUSTOM_TAB_SESSION_ID)?.let {
if (requireComponents.core.sessionManager.findSessionById(it)?.customTabConfig != null) { if (requireComponents.core.store.state.findCustomTab(it) != null) {
customTabSessionId = it customTabSessionId = it
} }
} }
@ -1033,10 +1036,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
/** /**
* Forwards activity results to the prompt feature. * Forwards activity results to the [ActivityResultHandler] features.
*/ */
final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
listOf( return listOf(
promptsFeature, promptsFeature,
webAuthnFeature webAuthnFeature
).any { it.onActivityResult(requestCode, data, resultCode) } ).any { it.onActivityResult(requestCode, data, resultCode) }
@ -1047,22 +1050,18 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
* or if it has a parent session and no more history * or if it has a parent session and no more history
*/ */
protected open fun removeSessionIfNeeded(): Boolean { protected open fun removeSessionIfNeeded(): Boolean {
getSessionById()?.let { session -> getCurrentTab()?.let { session ->
return if (session.source == SessionState.Source.ACTION_VIEW) { return if (session.source == SessionState.Source.ACTION_VIEW) {
activity?.finish() activity?.finish()
requireComponents.useCases.tabsUseCases.removeTab(session) requireComponents.useCases.tabsUseCases.removeTab(session.id)
true true
} else { } else {
if (session.hasParentSession) { val hasParentSession = session is TabSessionState && session.parentId != null
// The removeTab use case does not currently select a parent session, so if (hasParentSession) {
// we are using sessionManager.remove requireComponents.useCases.tabsUseCases.removeTab(session.id, selectParentIfExists = true)
requireComponents.core.sessionManager.remove(
session,
selectParentIfExists = true
)
} }
// We want to return to home if this session didn't have a parent session to select. // We want to return to home if this session didn't have a parent session to select.
val goToOverview = !session.hasParentSession val goToOverview = !hasParentSession
!goToOverview !goToOverview
} }
} }
@ -1127,20 +1126,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
(activity as HomeActivity).browsingModeManager.mode = sessionMode (activity as HomeActivity).browsingModeManager.mode = sessionMode
} }
/** @VisibleForTesting
* Returns the current session. internal fun getCurrentTab(): SessionState? {
*/
protected fun getSessionById(): Session? {
val sessionManager = requireComponents.core.sessionManager
val localCustomTabId = customTabSessionId
return if (localCustomTabId != null) {
sessionManager.findSessionById(localCustomTabId)
} else {
sessionManager.selectedSession
}
}
private fun getCurrentTab(): SessionState? {
return requireComponents.core.store.state.findCustomTabOrSelectedTab(customTabSessionId) return requireComponents.core.store.state.findCustomTabOrSelectedTab(customTabSessionId)
} }
@ -1228,12 +1215,14 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
.setText(getString(R.string.full_screen_notification)) .setText(getString(R.string.full_screen_notification))
.show() .show()
activity?.enterToImmersiveMode() activity?.enterToImmersiveMode()
browserToolbarView.collapse()
browserToolbarView.view.isVisible = false browserToolbarView.view.isVisible = false
val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams val browserEngine = swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
browserEngine.bottomMargin = 0 browserEngine.bottomMargin = 0
browserEngine.topMargin = 0
swipeRefresh.translationY = 0f
engineView.setDynamicToolbarMaxHeight(0) engineView.setDynamicToolbarMaxHeight(0)
browserToolbarView.expand()
// Without this, fullscreen has a margin at the top. // Without this, fullscreen has a margin at the top.
engineView.setVerticalClipping(0) engineView.setVerticalClipping(0)
@ -1247,6 +1236,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
browserToolbarView.view.isVisible = true browserToolbarView.view.isVisible = true
val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height) val toolbarHeight = resources.getDimensionPixelSize(R.dimen.browser_toolbar_height)
initializeEngineView(toolbarHeight) initializeEngineView(toolbarHeight)
browserToolbarView.expand()
} }
} }
@ -1310,10 +1300,16 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
override fun onAccessibilityStateChanged(enabled: Boolean) { override fun onAccessibilityStateChanged(enabled: Boolean) {
if (_browserToolbarView != null) { if (_browserToolbarView != null) {
browserToolbarView.setScrollFlags(enabled) browserToolbarView.setToolbarBehavior(enabled)
} }
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
_browserToolbarView?.dismissMenu()
}
// This method is called in response to native web extension messages from // This method is called in response to native web extension messages from
// content scripts (e.g the reader view extension). By the time these // content scripts (e.g the reader view extension). By the time these
// messages are processed the fragment/view may no longer be attached. // messages are processed the fragment/view may no longer be attached.
@ -1326,4 +1322,28 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler,
} }
} }
} }
/**
* Convenience method for replacing EngineView (id/engineView) in unit tests.
*/
@VisibleForTesting
internal fun getEngineView() = engineView
/**
* Convenience method for replacing SwipeRefreshLayout (id/swipeRefresh) in unit tests.
*/
@VisibleForTesting
internal fun getSwipeRefreshLayout() = swipeRefresh
@VisibleForTesting
internal fun shouldShowCompletedDownloadDialog(
downloadState: DownloadState,
status: DownloadState.Status
): Boolean {
val isValidStatus = status in listOf(DownloadState.Status.COMPLETED, DownloadState.Status.FAILED)
val isSameTab = downloadState.sessionId == getCurrentTab()?.id ?: false
return isValidStatus && isSameTab
}
} }

@ -69,7 +69,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
tabPreview = tabPreview, tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view, toolbarLayout = browserToolbarView.view,
store = components.core.store, store = components.core.store,
sessionManager = components.core.sessionManager selectTabUseCase = components.useCases.tabsUseCases.selectTab
) )
) )
} }
@ -84,7 +84,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
visible = { visible = {
readerModeAvailable readerModeAvailable
}, },
selected = getSessionById()?.let { selected = getCurrentTab()?.let {
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
} ?: false, } ?: false,
listener = browserInteractor::onReaderModePressed listener = browserInteractor::onReaderModePressed
@ -137,7 +137,8 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
navController = findNavController(), navController = findNavController(),
settings = context.settings(), settings = context.settings(),
appLinksUseCases = context.components.useCases.appLinksUseCases, appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserLayout as ViewGroup container = browserLayout as ViewGroup,
shouldScrollWithTopToolbar = !context.settings().shouldUseBottomToolbar
), ),
owner = this, owner = this,
view = view view = view

@ -29,6 +29,7 @@ class CustomTabContextMenuCandidate {
), ),
ContextMenuCandidate.createShareLinkCandidate(context), ContextMenuCandidate.createShareLinkCandidate(context),
ContextMenuCandidate.createSaveImageCandidate(context, contextMenuUseCases), ContextMenuCandidate.createSaveImageCandidate(context, contextMenuUseCases),
ContextMenuCandidate.createSaveVideoAudioCandidate(context, contextMenuUseCases),
ContextMenuCandidate.createCopyImageLocationCandidate( ContextMenuCandidate.createCopyImageLocationCandidate(
context, context,
snackBarParentView, snackBarParentView,

@ -22,6 +22,11 @@ import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.infobanner.DynamicInfoBanner
import org.mozilla.fenix.browser.infobanner.InfoBanner
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.Event.BannerOpenInAppGoToSettings
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -37,7 +42,9 @@ class OpenInAppOnboardingObserver(
private val navController: NavController, private val navController: NavController,
private val settings: Settings, private val settings: Settings,
private val appLinksUseCases: AppLinksUseCases, private val appLinksUseCases: AppLinksUseCases,
private val container: ViewGroup private val container: ViewGroup,
@VisibleForTesting
internal val shouldScrollWithTopToolbar: Boolean = false
) : LifecycleAwareFeature { ) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null private var scope: CoroutineScope? = null
private var currentUrl: String? = null private var currentUrl: String? = null
@ -85,22 +92,30 @@ class OpenInAppOnboardingObserver(
infoBanner?.showBanner() infoBanner?.showBanner()
sessionDomainForDisplayedBanner = url.tryGetHostFromUrl() sessionDomainForDisplayedBanner = url.tryGetHostFromUrl()
settings.shouldShowOpenInAppBanner = false settings.shouldShowOpenInAppBanner = false
context.components.analytics.metrics.track(Event.BannerOpenInAppDisplayed)
} }
} }
@VisibleForTesting @VisibleForTesting
internal fun createInfoBanner(): InfoBanner { internal fun createInfoBanner(): DynamicInfoBanner {
return InfoBanner( return DynamicInfoBanner(
context = context, context = context,
message = context.getString(R.string.open_in_app_cfr_info_message), message = context.getString(R.string.open_in_app_cfr_info_message),
dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text), dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text),
actionText = context.getString(R.string.open_in_app_cfr_positive_button_text), actionText = context.getString(R.string.open_in_app_cfr_positive_button_text),
container = container container = container,
shouldScrollWithTopToolbar = shouldScrollWithTopToolbar,
dismissAction = ::dismissAction
) { ) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment( val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment(
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app) preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app)
) )
context.components.analytics.metrics.track(BannerOpenInAppGoToSettings)
navController.nav(R.id.browserFragment, directions) navController.nav(R.id.browserFragment, directions)
} }
} }
private fun dismissAction() {
context.components.analytics.metrics.track(Event.BannerOpenInAppDismissed)
}
} }

@ -17,16 +17,16 @@ import androidx.core.graphics.contains
import androidx.core.graphics.toPoint import androidx.core.graphics.toPoint
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.ktx.android.view.getRectWithViewLocation import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getRectWithScreenLocation import org.mozilla.fenix.ext.getRectWithScreenLocation
import org.mozilla.fenix.ext.getWindowInsets import org.mozilla.fenix.ext.getWindowInsets
import org.mozilla.fenix.ext.isKeyboardVisible import org.mozilla.fenix.ext.isKeyboardVisible
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
@ -43,7 +43,7 @@ class ToolbarGestureHandler(
private val tabPreview: TabPreview, private val tabPreview: TabPreview,
private val toolbarLayout: View, private val toolbarLayout: View,
private val store: BrowserStore, private val store: BrowserStore,
private val sessionManager: SessionManager private val selectTabUseCase: TabsUseCases.SelectTabUseCase
) : SwipeGestureListener { ) : SwipeGestureListener {
private enum class GestureDirection { private enum class GestureDirection {
@ -51,7 +51,7 @@ class ToolbarGestureHandler(
} }
private sealed class Destination { private sealed class Destination {
data class Tab(val session: Session) : Destination() data class Tab(val tab: TabSessionState) : Destination()
object None : Destination() object None : Destination()
} }
@ -140,7 +140,7 @@ class ToolbarGestureHandler(
) { ) {
val destination = getDestination() val destination = getDestination()
if (destination is Destination.Tab && isGestureComplete(velocityX)) { if (destination is Destination.Tab && isGestureComplete(velocityX)) {
animateToNextTab(destination.session) animateToNextTab(destination.tab)
} else { } else {
animateCanceledGesture(velocityX) animateCanceledGesture(velocityX)
} }
@ -149,14 +149,14 @@ class ToolbarGestureHandler(
private fun getDestination(): Destination { private fun getDestination(): Destination {
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
val currentTab = store.state.selectedTab ?: return Destination.None val currentTab = store.state.selectedTab ?: return Destination.None
val currentIndex = sessionManager.sessionsOfType(currentTab.content.private).indexOfFirst { val currentIndex = store.state.getNormalOrPrivateTabs(currentTab.content.private).indexOfFirst {
it.id == currentTab.id it.id == currentTab.id
} }
return if (currentIndex == -1) { return if (currentIndex == -1) {
Destination.None Destination.None
} else { } else {
val sessions = sessionManager.sessionsOfType(currentTab.content.private) val tabs = store.state.getNormalOrPrivateTabs(currentTab.content.private)
val index = when (gestureDirection) { val index = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) { GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
currentIndex + 1 currentIndex + 1
@ -170,8 +170,8 @@ class ToolbarGestureHandler(
} }
} }
if (index < sessions.count() && index >= 0) { if (index < tabs.count() && index >= 0) {
Destination.Tab(sessions.elementAt(index)) Destination.Tab(tabs.elementAt(index))
} else { } else {
Destination.None Destination.None
} }
@ -180,7 +180,7 @@ class ToolbarGestureHandler(
private fun preparePreview(destination: Destination) { private fun preparePreview(destination: Destination) {
val thumbnailId = when (destination) { val thumbnailId = when (destination) {
is Destination.Tab -> destination.session.id is Destination.Tab -> destination.tab.id
is Destination.None -> return is Destination.None -> return
} }
@ -233,7 +233,7 @@ class ToolbarGestureHandler(
} }
} }
private fun animateToNextTab(session: Session) { private fun animateToNextTab(tab: TabSessionState) {
val browserFinalXCoordinate: Float = when (gestureDirection) { val browserFinalXCoordinate: Float = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset GestureDirection.RIGHT_TO_LEFT -> -windowWidth.toFloat() - previewOffset
GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset GestureDirection.LEFT_TO_RIGHT -> windowWidth.toFloat() + previewOffset
@ -243,7 +243,7 @@ class ToolbarGestureHandler(
getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply { getAnimator(browserFinalXCoordinate, FINISHED_GESTURE_ANIMATION_DURATION).apply {
doOnEnd { doOnEnd {
contentLayout.translationX = 0f contentLayout.translationX = 0f
sessionManager.select(session) selectTabUseCase(tab.id)
// Fade out the tab preview to prevent flickering // Fade out the tab preview to prevent flickering
val shortAnimationDuration = val shortAnimationDuration =

@ -1,62 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.browser
import android.app.Activity
import android.content.Intent
import android.content.IntentSender
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.activity.ActivityDelegate
import mozilla.components.support.base.feature.ActivityResultHandler
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.FeatureFlags
/**
* This implementation of the WebAuthnFeature is only for testing in a nightly signed build.
*
* This should always be behind the [FeatureFlags.webAuthFeature] nightly flag.
*/
class WebAuthnFeature(
private val engine: Engine,
private val activity: Activity
) : LifecycleAwareFeature, ActivityResultHandler {
val logger = Logger("WebAuthnFeature")
var requestCode = ACTIVITY_REQUEST_CODE
var resultCallback: ((Intent?) -> Unit)? = null
private val delegate = object : ActivityDelegate {
override fun startIntentSenderForResult(intent: IntentSender, onResult: (Intent?) -> Unit) {
val code = requestCode++
logger.info("Received activity delegate request with code: $code intent: $intent")
activity.startIntentSenderForResult(intent, code, null, 0, 0, 0)
resultCallback = onResult
}
}
override fun start() {
logger.info("Feature started.")
engine.registerActivityDelegate(delegate)
}
override fun stop() {
logger.info("Feature stopped.")
engine.unregisterActivityDelegate()
}
override fun onActivityResult(requestCode: Int, data: Intent?, resultCode: Int): Boolean {
logger.info("Received activity result with code: $requestCode\ndata: $data")
if (this.requestCode == requestCode) {
logger.info("Invoking callback!")
resultCallback?.invoke(data)
return true
}
return false
}
companion object {
const val ACTIVITY_REQUEST_CODE = 10
}
}

@ -0,0 +1,42 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.browser.infobanner
import android.content.Context
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
/**
* [InfoBanner] that will automatically scroll with the top [BrowserToolbar].
* Only to be used with [BrowserToolbar]s placed at the top of the screen.
*
* @param shouldScrollWithTopToolbar whether to follow the Y translation of the top toolbar or not
*/
@Suppress("LongParameterList")
class DynamicInfoBanner(
private val context: Context,
container: ViewGroup,
@VisibleForTesting
internal val shouldScrollWithTopToolbar: Boolean = false,
message: String,
dismissText: String,
actionText: String? = null,
dismissByHiding: Boolean = false,
dismissAction: (() -> Unit)? = null,
actionToPerform: (() -> Unit)? = null
) : InfoBanner(
context, container, message, dismissText, actionText, dismissByHiding, dismissAction, actionToPerform
) {
override fun showBanner() {
super.showBanner()
if (shouldScrollWithTopToolbar) {
(bannerLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior = DynamicInfoBannerBehavior(
context, null
)
}
}
}

@ -0,0 +1,50 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.browser.infobanner
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout
import mozilla.components.browser.toolbar.BrowserToolbar
/**
* A [CoordinatorLayout.Behavior] implementation to be used when placing [InfoBanner]
* below the BrowserToolbar with which is has to scroll.
*
* This Behavior will keep the Y translations of [InfoBanner] and the top [BrowserToolbar] in sync
* so that the banner will be shown between:
* - the top of the container, being translated over the initial toolbar height (toolbar fully collapsed)
* - immediately below the toolbar (toolbar fully expanded).
*/
class DynamicInfoBannerBehavior(
context: Context?,
attrs: AttributeSet?
) : CoordinatorLayout.Behavior<View>(context, attrs) {
@VisibleForTesting
internal var toolbarHeight: Int = 0
override fun layoutDependsOn(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
if (dependency::class == BrowserToolbar::class) {
toolbarHeight = dependency.height
setBannerYTranslation(child, dependency.translationY)
return true
}
return super.layoutDependsOn(parent, child, dependency)
}
override fun onDependentViewChanged(parent: CoordinatorLayout, child: View, dependency: View): Boolean {
setBannerYTranslation(child, dependency.translationY)
return true
}
@VisibleForTesting
internal fun setBannerYTranslation(banner: View, newYTranslation: Float) {
banner.translationY = toolbarHeight + newYTranslation
}
}

@ -2,15 +2,14 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.browser package org.mozilla.fenix.browser.infobanner
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View.GONE import android.view.View.GONE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import androidx.annotation.VisibleForTesting
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import kotlinx.android.synthetic.main.info_banner.view.* import kotlinx.android.synthetic.main.info_banner.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -27,7 +26,7 @@ import org.mozilla.fenix.ext.settings
* @param actionToPerform - The action to be performed on action button press * @param actionToPerform - The action to be performed on action button press
*/ */
@SuppressWarnings("LongParameterList") @SuppressWarnings("LongParameterList")
class InfoBanner( open class InfoBanner(
private val context: Context, private val context: Context,
private val container: ViewGroup, private val container: ViewGroup,
private val message: String, private val message: String,
@ -38,10 +37,11 @@ class InfoBanner(
private val actionToPerform: (() -> Unit)? = null private val actionToPerform: (() -> Unit)? = null
) { ) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
private val bannerLayout = LayoutInflater.from(context) @VisibleForTesting
internal val bannerLayout = LayoutInflater.from(context)
.inflate(R.layout.info_banner, null) .inflate(R.layout.info_banner, null)
internal fun showBanner() { internal open fun showBanner() {
bannerLayout.banner_info_message.text = message bannerLayout.banner_info_message.text = message
bannerLayout.dismiss.text = dismissText bannerLayout.dismiss.text = dismissText
@ -53,10 +53,6 @@ class InfoBanner(
container.addView(bannerLayout) container.addView(bannerLayout)
val params = bannerLayout.layoutParams as ViewGroup.LayoutParams
params.height = WRAP_CONTENT
params.width = MATCH_PARENT
bannerLayout.dismiss.setOnClickListener { bannerLayout.dismiss.setOnClickListener {
dismissAction?.invoke() dismissAction?.invoke()
if (dismissByHiding) { bannerLayout.visibility = GONE } else { dismiss() } if (dismissByHiding) { bannerLayout.visibility = GONE } else { dismiss() }

@ -15,7 +15,6 @@ import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged import org.mozilla.fenix.collections.CollectionCreationAction.StepChanged
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.getMediaStateForSession
import org.mozilla.fenix.ext.toShortUrl import org.mozilla.fenix.ext.toShortUrl
import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.Tab
@ -87,12 +86,11 @@ internal fun BrowserState.getTabs(
): List<Tab> { ): List<Tab> {
return tabIds return tabIds
?.mapNotNull { id -> findTab(id) } ?.mapNotNull { id -> findTab(id) }
?.map { it.toTab(this, publicSuffixList) } ?.map { it.toTab(publicSuffixList) }
.orEmpty() .orEmpty()
} }
private fun TabSessionState.toTab( private fun TabSessionState.toTab(
state: BrowserState,
publicSuffixList: PublicSuffixList publicSuffixList: PublicSuffixList
): Tab { ): Tab {
val url = readerState.activeUrl ?: content.url val url = readerState.activeUrl ?: content.url
@ -102,8 +100,7 @@ private fun TabSessionState.toTab(
hostname = url.toShortUrl(publicSuffixList), hostname = url.toShortUrl(publicSuffixList),
title = content.title, title = content.title,
selected = null, selected = null,
icon = content.icon, icon = content.icon
mediaState = state.getMediaStateForSession(this.id)
) )
} }

@ -5,6 +5,7 @@
package org.mozilla.fenix.collections package org.mozilla.fenix.collections
import android.os.Handler import android.os.Handler
import android.os.Looper
import android.text.InputFilter import android.text.InputFilter
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
@ -19,7 +20,6 @@ import androidx.transition.Transition
import androidx.transition.TransitionManager import androidx.transition.TransitionManager
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_collection_creation.* import kotlinx.android.synthetic.main.component_collection_creation.*
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.android.view.showKeyboard
@ -169,7 +169,7 @@ class CollectionCreationView(
text = context.getString(R.string.create_collection_name_collection) text = context.getString(R.string.create_collection_name_collection)
setOnClickListener { setOnClickListener {
name_collection_edittext.hideKeyboard() name_collection_edittext.hideKeyboard()
val handler = Handler() val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ handler.postDelayed({
interactor.onBackPressed(SaveCollectionStep.NameCollection) interactor.onBackPressed(SaveCollectionStep.NameCollection)
}, TRANSITION_DURATION) }, TRANSITION_DURATION)
@ -197,8 +197,7 @@ class CollectionCreationView(
sessionId = tab.id.toString(), sessionId = tab.id.toString(),
url = tab.url, url = tab.url,
hostname = tab.url.toShortUrl(publicSuffixList), hostname = tab.url.toShortUrl(publicSuffixList),
title = tab.title, title = tab.title
mediaState = MediaState.State.NONE
) )
}.let { tabs -> }.let { tabs ->
collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true) collectionCreationTabListAdapter.updateData(tabs, tabs.toSet(), true)
@ -216,7 +215,7 @@ class CollectionCreationView(
text = context.getString(R.string.collection_rename) text = context.getString(R.string.collection_rename)
setOnClickListener { setOnClickListener {
name_collection_edittext.hideKeyboard() name_collection_edittext.hideKeyboard()
val handler = Handler() val handler = Handler(Looper.getMainLooper())
handler.postDelayed({ handler.postDelayed({
interactor.onBackPressed(SaveCollectionStep.RenameCollection) interactor.onBackPressed(SaveCollectionStep.RenameCollection)
}, TRANSITION_DURATION) }, TRANSITION_DURATION)

@ -24,6 +24,7 @@ import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.LeanplumMetricsService import org.mozilla.fenix.components.metrics.LeanplumMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.experiments.createNimbus
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored

@ -56,6 +56,7 @@ class Components(private val context: Context) {
} }
val services by lazyMonitored { Services(context, backgroundServices.accountManager) } val services by lazyMonitored { Services(context, backgroundServices.accountManager) }
val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) } val core by lazyMonitored { Core(context, analytics.crashReporter, strictMode) }
@Suppress("Deprecation")
val useCases by lazyMonitored { val useCases by lazyMonitored {
UseCases( UseCases(
context, context,
@ -66,6 +67,7 @@ class Components(private val context: Context) {
core.topSitesStorage core.topSitesStorage
) )
} }
@Suppress("Deprecation")
val intentProcessors by lazyMonitored { val intentProcessors by lazyMonitored {
IntentProcessors( IntentProcessors(
context, context,

@ -18,7 +18,6 @@ import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.session.undo.UndoMiddleware
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
@ -35,7 +34,6 @@ import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DownloadMiddleware import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.MediaSessionFeature import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
@ -44,6 +42,8 @@ import mozilla.components.feature.recentlyclosed.RecentlyClosedMiddleware
import mozilla.components.feature.search.middleware.SearchMiddleware import mozilla.components.feature.search.middleware.SearchMiddleware
import mozilla.components.feature.search.region.RegionMiddleware import mozilla.components.feature.search.region.RegionMiddleware
import mozilla.components.feature.session.HistoryDelegate import mozilla.components.feature.session.HistoryDelegate
import mozilla.components.feature.session.middleware.LastAccessMiddleware
import mozilla.components.feature.session.middleware.undo.UndoMiddleware
import mozilla.components.feature.top.sites.DefaultTopSitesStorage import mozilla.components.feature.top.sites.DefaultTopSitesStorage
import mozilla.components.feature.top.sites.PinnedSiteStorage import mozilla.components.feature.top.sites.PinnedSiteStorage
import mozilla.components.feature.webcompat.WebCompatFeature import mozilla.components.feature.webcompat.WebCompatFeature
@ -61,7 +61,6 @@ import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.TelemetryMiddleware import org.mozilla.fenix.TelemetryMiddleware
@ -69,7 +68,6 @@ import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService import org.mozilla.fenix.media.MediaSessionService
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
@ -178,6 +176,7 @@ class Core(
val store by lazyMonitored { val store by lazyMonitored {
val middlewareList = val middlewareList =
mutableListOf( mutableListOf(
LastAccessMiddleware(),
RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine), RecentlyClosedMiddleware(context, RECENTLY_CLOSED_MAX, engine),
DownloadMiddleware(context, DownloadService::class.java), DownloadMiddleware(context, DownloadService::class.java),
ReaderViewMiddleware(), ReaderViewMiddleware(),
@ -197,19 +196,17 @@ class Core(
RecordingDevicesMiddleware(context) RecordingDevicesMiddleware(context)
) )
if (!newMediaSessionApi) {
middlewareList.add(MediaMiddleware(context, MediaService::class.java))
}
BrowserStore( BrowserStore(
middleware = middlewareList + EngineMiddleware.create(engine, ::findSessionById) middleware = middlewareList + EngineMiddleware.create(engine, ::findSessionById)
) )
} }
@Suppress("Deprecation")
private fun lookupSessionManager(): SessionManager { private fun lookupSessionManager(): SessionManager {
return sessionManager return sessionManager
} }
@Suppress("Deprecation")
private fun findSessionById(tabId: String): Session? { private fun findSessionById(tabId: String): Session? {
return sessionManager.findSessionById(tabId) return sessionManager.findSessionById(tabId)
} }
@ -232,6 +229,7 @@ class Core(
* sessions from the [SessionStorage], and with a default session (about:blank) in * sessions from the [SessionStorage], and with a default session (about:blank) in
* case all sessions/tabs are closed. * case all sessions/tabs are closed.
*/ */
@Deprecated("Use browser store (for reading) and use cases (for writing) instead")
val sessionManager by lazyMonitored { val sessionManager by lazyMonitored {
SessionManager(engine, store).also { SessionManager(engine, store).also {
// Install the "icons" WebExtension to automatically load icons for every visited website. // Install the "icons" WebExtension to automatically load icons for every visited website.
@ -248,9 +246,7 @@ class Core(
permissionStorage.permissionsStorage, HomeActivity::class.java permissionStorage.permissionsStorage, HomeActivity::class.java
) )
if (newMediaSessionApi) { MediaSessionFeature(context, MediaSessionService::class.java, store).start()
MediaSessionFeature(context, MediaSessionService::class.java, store).start()
}
} }
} }

@ -9,6 +9,7 @@ import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
@ -52,6 +53,7 @@ sealed class Event {
object CustomTabsActionTapped : Event() object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event() object CustomTabsMenuOpened : Event()
object UriOpened : Event() object UriOpened : Event()
object NormalAndPrivateUriOpened : Event()
object SyncAuthOpened : Event() object SyncAuthOpened : Event()
object SyncAuthClosed : Event() object SyncAuthClosed : Event()
object SyncAuthSignUp : Event() object SyncAuthSignUp : Event()
@ -198,12 +200,21 @@ sealed class Event {
object SyncedTabOpened : Event() object SyncedTabOpened : Event()
object RecentlyClosedTabsOpened : Event() object RecentlyClosedTabsOpened : Event()
object HaveOpenTabs : Event()
object HaveNoOpenTabs : Event()
object BannerOpenInAppDisplayed : Event()
object BannerOpenInAppDismissed : Event()
object BannerOpenInAppGoToSettings : Event()
object ContextMenuCopyTapped : Event() object ContextMenuCopyTapped : Event()
object ContextMenuSearchTapped : Event() object ContextMenuSearchTapped : Event()
object ContextMenuSelectAllTapped : Event() object ContextMenuSelectAllTapped : Event()
object ContextMenuShareTapped : Event() object ContextMenuShareTapped : Event()
object HaveTopSites : Event()
object HaveNoTopSites : Event()
// Interaction events with extras // Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() { data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -211,6 +222,25 @@ sealed class Event {
get() = hashMapOf(TopSites.swipeCarouselKeys.page to page.toString()) get() = hashMapOf(TopSites.swipeCarouselKeys.page to page.toString())
} }
data class SecurePrefsExperimentFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.experimentFailureKeys.failureException to failureException)
}
data class SecurePrefsGetFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.getFailureKeys.failureException to failureException)
}
data class SecurePrefsGetSuccess(val successCode: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.getResultKeys.result to successCode)
}
data class SecurePrefsWriteFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.writeFailureKeys.failureException to failureException)
}
object SecurePrefsWriteSuccess : Event()
object SecurePrefsReset : Event()
data class TopSiteLongPress(val type: TopSite.Type) : Event() { data class TopSiteLongPress(val type: TopSite.Type) : Event() {
override val extras: Map<TopSites.longPressKeys, String>? override val extras: Map<TopSites.longPressKeys, String>?
get() = hashMapOf(TopSites.longPressKeys.type to type.name) get() = hashMapOf(TopSites.longPressKeys.type to type.name)

@ -15,8 +15,10 @@ import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.GleanMetrics.AboutPage import org.mozilla.fenix.GleanMetrics.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment
import org.mozilla.fenix.GleanMetrics.AppTheme import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.BannerOpenInApp
import org.mozilla.fenix.GleanMetrics.BookmarksManagement import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
@ -236,6 +238,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.UriOpened -> EventWrapper<NoExtraKeys>( is Event.UriOpened -> EventWrapper<NoExtraKeys>(
{ Events.totalUriCount.add(1) } { Events.totalUriCount.add(1) }
) )
is Event.NormalAndPrivateUriOpened -> EventWrapper<NoExtraKeys>(
{ Events.normalAndPrivateUriCount.add(1) }
)
is Event.ErrorPageVisited -> EventWrapper( is Event.ErrorPageVisited -> EventWrapper(
{ ErrorPage.visitedError.record(it) }, { ErrorPage.visitedError.record(it) },
{ ErrorPage.visitedErrorKeys.valueOf(it) } { ErrorPage.visitedErrorKeys.valueOf(it) }
@ -723,6 +728,51 @@ private val Event.wrapper: EventWrapper<*>?
is Event.ContextMenuShareTapped -> EventWrapper<NoExtraKeys>( is Event.ContextMenuShareTapped -> EventWrapper<NoExtraKeys>(
{ ContextualMenu.shareTapped.record(it) } { ContextualMenu.shareTapped.record(it) }
) )
Event.HaveOpenTabs -> EventWrapper<NoExtraKeys>(
{ Metrics.hasOpenTabs.set(true) }
)
Event.HaveNoOpenTabs -> EventWrapper<NoExtraKeys>(
{ Metrics.hasOpenTabs.set(false) }
)
Event.HaveTopSites -> EventWrapper<NoExtraKeys>(
{ Metrics.hasTopSites.set(true) }
)
Event.HaveNoTopSites -> EventWrapper<NoExtraKeys>(
{ Metrics.hasTopSites.set(false) }
)
is Event.BannerOpenInAppDisplayed -> EventWrapper<NoExtraKeys>(
{ BannerOpenInApp.displayed.record(it) }
)
is Event.BannerOpenInAppDismissed -> EventWrapper<NoExtraKeys>(
{ BannerOpenInApp.dismissed.record(it) }
)
is Event.BannerOpenInAppGoToSettings -> EventWrapper<NoExtraKeys>(
{ BannerOpenInApp.goToSettings.record(it) }
)
is Event.SecurePrefsExperimentFailure -> EventWrapper(
{ AndroidKeystoreExperiment.experimentFailure.record(it) },
{ AndroidKeystoreExperiment.experimentFailureKeys.valueOf(it) }
)
is Event.SecurePrefsGetFailure -> EventWrapper(
{ AndroidKeystoreExperiment.getFailure.record(it) },
{ AndroidKeystoreExperiment.getFailureKeys.valueOf(it) }
)
is Event.SecurePrefsGetSuccess -> EventWrapper(
{ AndroidKeystoreExperiment.getResult.record(it) },
{ AndroidKeystoreExperiment.getResultKeys.valueOf(it) }
)
is Event.SecurePrefsWriteFailure -> EventWrapper(
{ AndroidKeystoreExperiment.writeFailure.record(it) },
{ AndroidKeystoreExperiment.writeFailureKeys.valueOf(it) }
)
is Event.SecurePrefsWriteSuccess -> EventWrapper<NoExtraKeys>(
{ AndroidKeystoreExperiment.writeSuccess.record(it) }
)
is Event.SecurePrefsReset -> EventWrapper<NoExtraKeys>(
{ AndroidKeystoreExperiment.reset.record(it) }
)
// Don't record other events in Glean: // Don't record other events in Glean:
is Event.AddBookmark -> null is Event.AddBookmark -> null

@ -22,6 +22,8 @@ import mozilla.components.feature.findinpage.facts.FindInPageFacts
import mozilla.components.feature.media.facts.MediaFacts import mozilla.components.feature.media.facts.MediaFacts
import mozilla.components.feature.prompts.dialog.LoginDialogFacts import mozilla.components.feature.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
import mozilla.components.support.base.Component import mozilla.components.support.base.Component
import mozilla.components.support.base.facts.Action import mozilla.components.support.base.facts.Action
import mozilla.components.support.base.facts.Fact import mozilla.components.support.base.facts.Fact
@ -75,6 +77,7 @@ internal class DebugMetricController(
} }
@VisibleForTesting @VisibleForTesting
@Suppress("LargeClass")
internal class ReleaseMetricController( internal class ReleaseMetricController(
private val services: List<MetricsService>, private val services: List<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean, private val isDataTelemetryEnabled: () -> Boolean,
@ -134,6 +137,13 @@ internal class ReleaseMetricController(
} }
} }
@VisibleForTesting
internal fun factToEvent(
fact: Fact
): Event? {
return fact.toEvent()
}
private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type) private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type)
private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) { private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) {
@ -242,6 +252,43 @@ internal class ReleaseMetricController(
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> { Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> {
Event.ProgressiveWebAppInstallAsShortcut Event.ProgressiveWebAppInstallAsShortcut
} }
Component.FEATURE_TOP_SITES to TopSitesFacts.Items.COUNT -> {
value?.let {
var count = 0
try {
count = it.toInt()
} catch (e: NumberFormatException) {
// Do nothing
}
return if (count > 0) {
Event.HaveTopSites
} else {
Event.HaveNoTopSites
}
}
null
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT -> {
Event.SecurePrefsExperimentFailure(metadata?.get("javaClass") as String? ?: "null")
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.GET -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsGetFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsGetSuccess(value ?: "")
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.WRITE -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsWriteFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsWriteSuccess
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.RESET -> {
Event.SecurePrefsReset
}
else -> null else -> null
} }

@ -0,0 +1,35 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.metrics
import android.content.Context
import android.os.Build
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
import mozilla.components.service.nimbus.NimbusApi
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
import org.mozilla.fenix.ext.withExperiment
/**
* Allows starting a quick test of ACs SecureAbove22Preferences that will emit Facts
* for the basic operations and allow us to log them for later evaluation of APIs stability.
*/
class SecurePrefsTelemetry(
private val appContext: Context,
private val experiments: NimbusApi
) {
suspend fun startTests() {
// The Android Keystore is used to secure the shared prefs only on API 23+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// These tests should run only if the experiment is live
experiments.withExperiment(Experiments.ANDROID_KEYSTORE) { experimentBranch ->
// .. and this device is not in the control group.
if (experimentBranch == ExperimentBranch.TREATMENT) {
SecurePrefsReliabilityExperiment(appContext)()
}
}
}
}
}

@ -5,11 +5,14 @@
package org.mozilla.fenix.components.toolbar package org.mozilla.fenix.components.toolbar
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineView import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import mozilla.components.ui.tabcounter.TabCounterMenu import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -22,7 +25,6 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
@ -41,20 +43,20 @@ interface BrowserToolbarController {
class DefaultBrowserToolbarController( class DefaultBrowserToolbarController(
private val store: BrowserStore, private val store: BrowserStore,
private val tabsUseCases: TabsUseCases,
private val activity: HomeActivity, private val activity: HomeActivity,
private val navController: NavController, private val navController: NavController,
private val metrics: MetricController, private val metrics: MetricController,
private val readerModeController: ReaderModeController, private val readerModeController: ReaderModeController,
private val sessionManager: SessionManager,
private val engineView: EngineView, private val engineView: EngineView,
private val homeViewModel: HomeScreenViewModel, private val homeViewModel: HomeScreenViewModel,
private val customTabSession: Session?, private val customTabSessionId: String?,
private val onTabCounterClicked: () -> Unit, private val onTabCounterClicked: () -> Unit,
private val onCloseTab: (Session) -> Unit private val onCloseTab: (SessionState) -> Unit
) : BrowserToolbarController { ) : BrowserToolbarController {
private val currentSession private val currentSession
get() = customTabSession ?: sessionManager.selectedSession get() = store.state.findCustomTabOrSelectedTab(customTabSessionId)
override fun handleToolbarPaste(text: String) { override fun handleToolbarPaste(text: String) {
navController.nav( navController.nav(
@ -112,18 +114,16 @@ class DefaultBrowserToolbarController(
metrics.track( metrics.track(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB) Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.CLOSE_TAB)
) )
sessionManager.selectedSession?.let { store.state.selectedTab?.let {
// When closing the last tab we must show the undo snackbar in the home fragment // When closing the last tab we must show the undo snackbar in the home fragment
if (sessionManager.sessionsOfType(it.private).count() == 1) { if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) {
homeViewModel.sessionToDelete = it.id homeViewModel.sessionToDelete = it.id
navController.navigate( navController.navigate(
BrowserFragmentDirections.actionGlobalHome() BrowserFragmentDirections.actionGlobalHome()
) )
} else { } else {
onCloseTab.invoke(it) onCloseTab.invoke(it)
// The removeTab use case does not currently select a parent session, so tabsUseCases.removeTab(it.id, selectParentIfExists = true)
// we are using sessionManager.remove
sessionManager.remove(it, selectParentIfExists = true)
} }
} }
} }

@ -15,9 +15,10 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.concept.engine.EngineSession.LoadUrlFlags
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
@ -51,19 +52,19 @@ interface BrowserToolbarMenuController {
fun handleToolbarItemInteraction(item: ToolbarMenu.Item) fun handleToolbarItemInteraction(item: ToolbarMenu.Item)
} }
@Suppress("LargeClass") @Suppress("LargeClass", "ForbiddenComment")
class DefaultBrowserToolbarMenuController( class DefaultBrowserToolbarMenuController(
private val store: BrowserStore,
private val activity: HomeActivity, private val activity: HomeActivity,
private val navController: NavController, private val navController: NavController,
private val metrics: MetricController, private val metrics: MetricController,
private val settings: Settings, private val settings: Settings,
private val readerModeController: ReaderModeController, private val readerModeController: ReaderModeController,
private val sessionFeature: ViewBoundFeatureWrapper<SessionFeature>, private val sessionFeature: ViewBoundFeatureWrapper<SessionFeature>,
private val sessionManager: SessionManager,
private val findInPageLauncher: () -> Unit, private val findInPageLauncher: () -> Unit,
private val browserAnimator: BrowserAnimator, private val browserAnimator: BrowserAnimator,
private val swipeRefresh: SwipeRefreshLayout, private val swipeRefresh: SwipeRefreshLayout,
private val customTabSession: Session?, private val customTabSessionId: String?,
private val openInFenixIntent: Intent, private val openInFenixIntent: Intent,
private val bookmarkTapped: (String, String) -> Unit, private val bookmarkTapped: (String, String) -> Unit,
private val scope: CoroutineScope, private val scope: CoroutineScope,
@ -73,7 +74,7 @@ class DefaultBrowserToolbarMenuController(
) : BrowserToolbarMenuController { ) : BrowserToolbarMenuController {
private val currentSession private val currentSession
get() = customTabSession ?: sessionManager.selectedSession get() = store.state.findCustomTabOrSelectedTab(customTabSessionId)
// We hold onto a reference of the inner scope so that we can override this with the // We hold onto a reference of the inner scope so that we can override this with the
// TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests // TestCoroutineScope to ensure sequential execution. If we didn't have this, our tests
@ -84,29 +85,105 @@ class DefaultBrowserToolbarMenuController(
@Suppress("ComplexMethod", "LongMethod") @Suppress("ComplexMethod", "LongMethod")
override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) { override fun handleToolbarItemInteraction(item: ToolbarMenu.Item) {
val sessionUseCases = activity.components.useCases.sessionUseCases val sessionUseCases = activity.components.useCases.sessionUseCases
val customTabUseCases = activity.components.useCases.customTabsUseCases
trackToolbarItemInteraction(item) trackToolbarItemInteraction(item)
Do exhaustive when (item) { Do exhaustive when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.InstallToHomeScreen -> {
settings.installPwaOpened = true
MainScope().launch {
with(activity.components.useCases.webAppUseCases) {
if (isInstallable()) {
addToHomescreen()
} else {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToCreateShortcutFragment()
navController.navigateSafe(R.id.browserFragment, directions)
}
}
}
}
is ToolbarMenu.Item.OpenInFenix -> {
customTabSessionId?.let {
// Stop the SessionFeature from updating the EngineView and let it release the session
// from the EngineView so that it can immediately be rendered by a different view once
// we switch to the actual browser.
sessionFeature.get()?.release()
// Turn this Session into a regular tab and then select it
customTabUseCases.migrate(customTabSessionId, select = true)
// Switch to the actual browser which should now display our new selected session
activity.startActivity(openInFenixIntent.apply {
// We never want to launch the browser in the same task as the external app
// activity. So we force a new task here. IntentReceiverActivity will do the
// right thing and take care of routing to an already existing browser and avoid
// cloning a new one.
flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK
})
// Close this activity (and the task) since it is no longer displaying any session
activity.finishAndRemoveTask()
}
}
is ToolbarMenu.Item.Quit -> {
// We need to show the snackbar while the browsing data is deleting (if "Delete
// browsing data on quit" is activated). After the deletion is over, the snackbar
// is dismissed.
val snackbar: FenixSnackbar? = activity.getRootView()?.let { v ->
FenixSnackbar.make(
view = v,
duration = Snackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true
)
.setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
}
deleteAndQuit(activity, scope, snackbar)
}
is ToolbarMenu.Item.ReaderModeAppearance -> {
readerModeController.showControls()
metrics.track(Event.ReaderModeAppearanceOpened)
}
is ToolbarMenu.Item.OpenInApp -> {
settings.openInAppOpened = true
val appLinksUseCases = activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
currentSession?.let {
val redirect = getRedirect.invoke(it.content.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
}
}
// todo === End ===
is ToolbarMenu.Item.Back -> { is ToolbarMenu.Item.Back -> {
if (item.viewHistory) { if (item.viewHistory) {
navController.navigate( navController.navigate(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSession?.id activeSessionId = customTabSessionId
) )
) )
} else { } else {
sessionUseCases.goBack.invoke(currentSession) currentSession?.let {
sessionUseCases.goBack.invoke(it.id)
}
} }
} }
is ToolbarMenu.Item.Forward -> { is ToolbarMenu.Item.Forward -> {
if (item.viewHistory) { if (item.viewHistory) {
navController.navigate( navController.navigate(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSession?.id activeSessionId = customTabSessionId
) )
) )
} else { } else {
sessionUseCases.goForward.invoke(currentSession) currentSession?.let {
sessionUseCases.goForward.invoke(it.id)
}
} }
} }
is ToolbarMenu.Item.Reload -> { is ToolbarMenu.Item.Reload -> {
@ -116,24 +193,46 @@ class DefaultBrowserToolbarMenuController(
LoadUrlFlags.none() LoadUrlFlags.none()
} }
sessionUseCases.reload.invoke(currentSession, flags = flags) currentSession?.let {
sessionUseCases.reload.invoke(it.id, flags = flags)
}
}
is ToolbarMenu.Item.Stop -> {
currentSession?.let {
sessionUseCases.stopLoading.invoke(it.id)
}
} }
ToolbarMenu.Item.Stop -> sessionUseCases.stopLoading.invoke(currentSession) is ToolbarMenu.Item.Share -> {
ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { val directions = NavGraphDirections.actionGlobalShareFragment(
data = arrayOf(
ShareData(
url = getProperUrl(currentSession),
title = currentSession?.content?.title
)
),
showPage = true
)
navController.navigate(directions)
}
is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
navController.nav(R.id.browserFragment, directions) navController.nav(R.id.browserFragment, directions)
} }
ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically { is ToolbarMenu.Item.SyncedTabs -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav( navController.nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment() BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment()
) )
} }
is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke( is ToolbarMenu.Item.RequestDesktop -> {
item.isChecked, currentSession?.let {
currentSession sessionUseCases.requestDesktopSite.invoke(
) item.isChecked,
ToolbarMenu.Item.AddToTopSites -> { it.id
)
}
}
is ToolbarMenu.Item.AddToTopSites -> {
scope.launch { scope.launch {
val context = swipeRefresh.context val context = swipeRefresh.context
val numPinnedSites = val numPinnedSites =
@ -152,7 +251,7 @@ class DefaultBrowserToolbarMenuController(
ioScope.launch { ioScope.launch {
currentSession?.let { currentSession?.let {
with(activity.components.useCases.topSitesUseCase) { with(activity.components.useCases.topSitesUseCase) {
addPinnedSites(it.title, it.url) addPinnedSites(it.content.title, it.content.url)
} }
} }
}.join() }.join()
@ -169,7 +268,7 @@ class DefaultBrowserToolbarMenuController(
} }
} }
} }
ToolbarMenu.Item.AddToHomeScreen, ToolbarMenu.Item.InstallToHomeScreen -> { is ToolbarMenu.Item.AddToHomeScreen -> {
settings.installPwaOpened = true settings.installPwaOpened = true
MainScope().launch { MainScope().launch {
with(activity.components.useCases.webAppUseCases) { with(activity.components.useCases.webAppUseCases) {
@ -183,31 +282,17 @@ class DefaultBrowserToolbarMenuController(
} }
} }
} }
ToolbarMenu.Item.Share -> { is ToolbarMenu.Item.FindInPage -> {
val directions = NavGraphDirections.actionGlobalShareFragment(
data = arrayOf(
ShareData(
url = getProperUrl(currentSession),
title = currentSession?.title
)
),
showPage = true
)
navController.navigate(directions)
}
ToolbarMenu.Item.FindInPage -> {
findInPageLauncher() findInPageLauncher()
metrics.track(Event.FindInPageOpened) metrics.track(Event.FindInPageOpened)
} }
is ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically {
ToolbarMenu.Item.AddonsManager -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav( navController.nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionGlobalAddonsManagementFragment() BrowserFragmentDirections.actionGlobalAddonsManagementFragment()
) )
} }
ToolbarMenu.Item.SaveToCollection -> { is ToolbarMenu.Item.SaveToCollection -> {
metrics metrics
.track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER)) .track(Event.CollectionSaveButtonPressed(TELEMETRY_BROWSER_IDENTIFIER))
@ -225,92 +310,45 @@ class DefaultBrowserToolbarMenuController(
navController.nav(R.id.browserFragment, directions) navController.nav(R.id.browserFragment, directions)
} }
} }
ToolbarMenu.Item.OpenInFenix -> { is ToolbarMenu.Item.Bookmark -> {
// Stop the SessionFeature from updating the EngineView and let it release the session store.state.selectedTab?.let {
// from the EngineView so that it can immediately be rendered by a different view once getProperUrl(it)?.let { url -> bookmarkTapped(url, it.content.title) }
// we switch to the actual browser.
sessionFeature.get()?.release()
// Strip the CustomTabConfig to turn this Session into a regular tab and then select it
customTabSession!!.customTabConfig = null
sessionManager.select(customTabSession)
// Switch to the actual browser which should now display our new selected session
activity.startActivity(openInFenixIntent.apply {
// We never want to launch the browser in the same task as the external app
// activity. So we force a new task here. IntentReceiverActivity will do the
// right thing and take care of routing to an already existing browser and avoid
// cloning a new one.
flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK
})
// Close this activity (and the task) since it is no longer displaying any session
activity.finishAndRemoveTask()
}
ToolbarMenu.Item.Quit -> {
// We need to show the snackbar while the browsing data is deleting (if "Delete
// browsing data on quit" is activated). After the deletion is over, the snackbar
// is dismissed.
val snackbar: FenixSnackbar? = activity.getRootView()?.let { v ->
FenixSnackbar.make(
view = v,
duration = Snackbar.LENGTH_LONG,
isDisplayedWithBrowserToolbar = true
)
.setText(v.context.getString(R.string.deleting_browsing_data_in_progress))
}
deleteAndQuit(activity, scope, snackbar)
}
ToolbarMenu.Item.ReaderModeAppearance -> {
readerModeController.showControls()
metrics.track(Event.ReaderModeAppearanceOpened)
}
ToolbarMenu.Item.OpenInApp -> {
settings.openInAppOpened = true
val appLinksUseCases = activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
currentSession?.let {
val redirect = getRedirect.invoke(it.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
}
}
ToolbarMenu.Item.Bookmark -> {
sessionManager.selectedSession?.let {
getProperUrl(it)?.let { url -> bookmarkTapped(url, it.title) }
} }
} }
ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically { is ToolbarMenu.Item.Bookmarks -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav( navController.nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id) BrowserFragmentDirections.actionGlobalBookmarkFragment(BookmarkRoot.Mobile.id)
) )
} }
ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically { is ToolbarMenu.Item.History -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav( navController.nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionGlobalHistoryFragment() BrowserFragmentDirections.actionGlobalHistoryFragment()
) )
} }
ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically { is ToolbarMenu.Item.Downloads -> browserAnimator.captureEngineViewAndDrawStatically {
navController.nav( navController.nav(
R.id.browserFragment, R.id.browserFragment,
BrowserFragmentDirections.actionGlobalDownloadsFragment() BrowserFragmentDirections.actionGlobalDownloadsFragment()
) )
} }
is ToolbarMenu.Item.NewTab -> {
navController.navigate(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
} }
} }
private fun getProperUrl(currentSession: Session?): String? { private fun getProperUrl(currentSession: SessionState?): String? {
return currentSession?.id?.let { return currentSession?.id?.let {
val currentTab = browserStore.state.findTab(it) val currentTab = browserStore.state.findTab(it)
if (currentTab?.readerState?.active == true) { if (currentTab?.readerState?.active == true) {
currentTab.readerState.activeUrl currentTab.readerState.activeUrl
} else { } else {
currentSession.url currentSession.content.url
} }
} }
} }
@ -318,35 +356,38 @@ class DefaultBrowserToolbarMenuController(
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
val eventItem = when (item) { val eventItem = when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
is ToolbarMenu.Item.ReaderModeAppearance ->
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
// todo === End ===
is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP is ToolbarMenu.Item.Stop -> Event.BrowserMenuItemTapped.Item.STOP
ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS is ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE
is ToolbarMenu.Item.Settings -> Event.BrowserMenuItemTapped.Item.SETTINGS
is ToolbarMenu.Item.RequestDesktop -> is ToolbarMenu.Item.RequestDesktop ->
if (item.isChecked) { if (item.isChecked) {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_ON
} else { } else {
Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF Event.BrowserMenuItemTapped.Item.DESKTOP_VIEW_OFF
} }
is ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE
ToolbarMenu.Item.FindInPage -> Event.BrowserMenuItemTapped.Item.FIND_IN_PAGE is ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION
ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES
ToolbarMenu.Item.Share -> Event.BrowserMenuItemTapped.Item.SHARE is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
ToolbarMenu.Item.SaveToCollection -> Event.BrowserMenuItemTapped.Item.SAVE_TO_COLLECTION is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS
ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
ToolbarMenu.Item.ReaderModeAppearance -> is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
} }
metrics.track(Event.BrowserMenuItemTapped(eventItem)) metrics.track(Event.BrowserMenuItemTapped(eventItem))

@ -9,25 +9,18 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.annotation.VisibleForTesting
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
import com.google.android.material.appbar.AppBarLayout.LayoutParams.SCROLL_FLAG_SNAP
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.browser.toolbar.behavior.BrowserToolbarBehavior
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.support.utils.URLStringUtils import mozilla.components.support.utils.URLStringUtils
import mozilla.components.ui.tabcounter.TabCounterMenu import mozilla.components.ui.tabcounter.TabCounterMenu
@ -40,6 +33,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.ToolbarPopupWindow import org.mozilla.fenix.utils.ToolbarPopupWindow
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import mozilla.components.browser.toolbar.behavior.ToolbarPosition as MozacToolbarPosition
interface BrowserToolbarViewInteractor { interface BrowserToolbarViewInteractor {
fun onBrowserToolbarPaste(text: String) fun onBrowserToolbarPaste(text: String)
@ -58,7 +52,7 @@ class BrowserToolbarView(
private val container: ViewGroup, private val container: ViewGroup,
private val toolbarPosition: ToolbarPosition, private val toolbarPosition: ToolbarPosition,
private val interactor: BrowserToolbarViewInteractor, private val interactor: BrowserToolbarViewInteractor,
private val customTabSession: Session?, private val customTabSession: CustomTabSessionState?,
private val lifecycleOwner: LifecycleOwner private val lifecycleOwner: LifecycleOwner
) : LayoutContainer { ) : LayoutContainer {
@ -76,14 +70,16 @@ class BrowserToolbarView(
private val layout = LayoutInflater.from(container.context) private val layout = LayoutInflater.from(container.context)
.inflate(toolbarLayout, container, true) .inflate(toolbarLayout, container, true)
val view: BrowserToolbar = layout @VisibleForTesting
internal var view: BrowserToolbar = layout
.findViewById(R.id.toolbar) .findViewById(R.id.toolbar)
val toolbarIntegration: ToolbarIntegration val toolbarIntegration: ToolbarIntegration
private val isPwaTabOrTwaTab: Boolean @VisibleForTesting
get() = customTabSession?.customTabConfig?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP || internal val isPwaTabOrTwaTab: Boolean
customTabSession?.customTabConfig?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY get() = customTabSession?.config?.externalAppType == ExternalAppType.PROGRESSIVE_WEB_APP ||
customTabSession?.config?.externalAppType == ExternalAppType.TRUSTED_WEB_ACTIVITY
init { init {
val isCustomTabSession = customTabSession != null val isCustomTabSession = customTabSession != null
@ -101,17 +97,8 @@ class BrowserToolbarView(
with(container.context) { with(container.context) {
val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported() val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported()
if (toolbarPosition == ToolbarPosition.TOP) {
val offsetChangedListener =
AppBarLayout.OnOffsetChangedListener { _: AppBarLayout?, verticalOffset: Int ->
interactor.onScrolled(verticalOffset)
}
app_bar.addOnOffsetChangedListener(offsetChangedListener)
}
view.apply { view.apply {
setScrollFlags() setToolbarBehavior()
elevation = resources.getDimension(R.dimen.browser_fragment_toolbar_elevation) elevation = resources.getDimension(R.dimen.browser_fragment_toolbar_elevation)
@ -203,7 +190,7 @@ class BrowserToolbarView(
view, view,
menuToolbar, menuToolbar,
customTabSession.id, customTabSession.id,
isPrivate = customTabSession.private isPrivate = customTabSession.content.private
) )
} else { } else {
DefaultToolbarIntegration( DefaultToolbarIntegration(
@ -227,55 +214,76 @@ class BrowserToolbarView(
if (isPwaTabOrTwaTab) { if (isPwaTabOrTwaTab) {
return return
} }
when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> { (view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { (behavior as? BrowserToolbarBehavior)?.forceExpand(view)
// behavior can be null if the "Scroll to hide toolbar" setting is toggled off. }
(behavior as? BrowserToolbarBottomBehavior)?.forceExpand(view) }
}
} fun collapse() {
ToolbarPosition.TOP -> { // collapse only for normal tabs and custom tabs not for PWA or TWA. Mirror expand()
layout.app_bar?.setExpanded(true) if (isPwaTabOrTwaTab) {
} return
}
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
(behavior as? BrowserToolbarBehavior)?.forceCollapse(view)
} }
} }
fun dismissMenu() {
view.dismissMenu()
}
/** /**
* Dynamically sets scroll flags for the toolbar when the user does not have a screen reader enabled * Sets whether the toolbar will have a dynamic behavior (to be scrolled) or not.
* Note that the toolbar will have the flags set and be able to be hidden *
* only if the user didn't disabled this behavior in app's settings. * This will intrinsically check and disable the dynamic behavior if
* - this is disabled in app settings
* - toolbar is placed at the bottom and tab shows a PWA or TWA
*
* Also if the user has not explicitly set a toolbar position and has a screen reader enabled
* the toolbar will be placed at the top and in a fixed position.
*
* @param shouldDisableScroll force disable of the dynamic behavior irrespective of the intrinsic checks.
*/ */
fun setScrollFlags(shouldDisableScroll: Boolean = false) { fun setToolbarBehavior(shouldDisableScroll: Boolean = false) {
when (settings.toolbarPosition) { when (settings.toolbarPosition) {
ToolbarPosition.BOTTOM -> { ToolbarPosition.BOTTOM -> {
if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab) { if (settings.isDynamicToolbarEnabled && !isPwaTabOrTwaTab && !settings.shouldUseFixedTopToolbar) {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply { setDynamicToolbarBehavior(MozacToolbarPosition.BOTTOM)
behavior = BrowserToolbarBottomBehavior(view.context, null)
}
} else { } else {
expand() expandToolbarAndMakeItFixed()
} }
} }
ToolbarPosition.TOP -> { ToolbarPosition.TOP -> {
view.updateLayoutParams<AppBarLayout.LayoutParams> { if (settings.shouldUseFixedTopToolbar ||
scrollFlags = !settings.isDynamicToolbarEnabled ||
if (settings.shouldUseFixedTopToolbar || shouldDisableScroll
!settings.isDynamicToolbarEnabled || ) {
shouldDisableScroll) { expandToolbarAndMakeItFixed()
// Force expand the toolbar so the user is not stuck with a hidden toolbar } else {
expand() setDynamicToolbarBehavior(MozacToolbarPosition.TOP)
0
} else {
SCROLL_FLAG_SCROLL or
SCROLL_FLAG_ENTER_ALWAYS or
SCROLL_FLAG_SNAP or
SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
}
} }
} }
} }
} }
@VisibleForTesting
internal fun expandToolbarAndMakeItFixed() {
expand()
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
behavior = null
}
}
@VisibleForTesting
internal fun setDynamicToolbarBehavior(toolbarPosition: MozacToolbarPosition) {
(view.layoutParams as? CoordinatorLayout.LayoutParams)?.apply {
behavior = BrowserToolbarBehavior(view.context, null, toolbarPosition)
}
}
@Suppress("ComplexCondition") @Suppress("ComplexCondition")
private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) { private fun ToolbarMenu.Item.performHapticIfNeeded(view: View) {
if (this is ToolbarMenu.Item.Reload && this.bypassCache || if (this is ToolbarMenu.Item.Reload && this.bypassCache ||

@ -32,6 +32,7 @@ import mozilla.components.feature.webcompat.reporter.WebCompatReporterFeature
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@ -69,7 +70,12 @@ class DefaultToolbarMenu(
override val menuBuilder by lazy { override val menuBuilder by lazy {
WebExtensionBrowserMenuBuilder( WebExtensionBrowserMenuBuilder(
menuItems, items =
if (FeatureFlags.toolbarMenuFeature) {
newCoreMenuItems
} else {
oldCoreMenuItems
},
endOfMenuAlwaysVisible = !shouldReverseItems, endOfMenuAlwaysVisible = !shouldReverseItems,
store = store, store = store,
webExtIconTintColorResource = primaryTextColor(), webExtIconTintColorResource = primaryTextColor(),
@ -179,20 +185,159 @@ class DefaultToolbarMenu(
} ?: false } ?: false
// End of predicates // // End of predicates //
private val menuItems by lazy { private val oldCoreMenuItems by lazy {
val settings = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings),
startImageResource = R.drawable.ic_settings,
iconTintColorResource = if (hasAccountProblem)
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else
primaryTextColor(),
textColorResource = if (hasAccountProblem)
ThemeManager.resolveAttribute(R.attr.primaryText, context) else
primaryTextColor(),
highlight = BrowserMenuHighlight.HighPriority(
endImageResource = R.drawable.ic_sync_disconnected,
backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground),
canPropagate = false
),
isHighlighted = { hasAccountProblem }
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
val desktopMode = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
initialState = {
selectedSession?.content?.desktopMode ?: false
}
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
val addToTopSites = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_top_sites),
imageResource = R.drawable.ic_top_sites,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
}
val addToHomescreen = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.ic_add_to_homescreen,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
}
val syncedTabs = BrowserMenuImageText(
label = context.getString(R.string.synced_tabs),
imageResource = R.drawable.ic_synced_tabs,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
}
val installToHomescreen = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_install_on_homescreen),
startImageResource = R.drawable.ic_add_to_homescreen,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_install_on_homescreen),
notificationTint = getColor(context, R.color.whats_new_notification_color)
),
isHighlighted = {
!context.settings().installPwaOpened
}
) {
onItemTapped.invoke(ToolbarMenu.Item.InstallToHomeScreen)
}
val findInPage = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID
)
val saveToCollection = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2),
imageResource = R.drawable.ic_tab_collection,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
}
val deleteDataOnQuit = BrowserMenuImageText(
label = context.getString(R.string.delete_browsing_data_on_quit_action),
imageResource = R.drawable.ic_exit,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Quit)
}
val readerAppearance = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_read_appearance),
imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance)
}
val openInApp = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_open_app_link),
startImageResource = R.drawable.ic_open_in_app,
iconTintColorResource = primaryTextColor(),
highlight = BrowserMenuHighlight.LowPriority(
label = context.getString(R.string.browser_menu_open_app_link),
notificationTint = getColor(context, R.color.whats_new_notification_color)
),
isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val historyItem = BrowserMenuImageText(
context.getString(R.string.library_history),
R.drawable.ic_history,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.History)
}
val bookmarksItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks),
R.drawable.ic_bookmark_filled,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
}
val downloadsItem = BrowserMenuImageText(
context.getString(R.string.library_downloads),
R.drawable.ic_download,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
// Predicates that are called once, during screen init // Predicates that are called once, during screen init
val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity) val shouldShowSaveToCollection = (context.asActivity() as? HomeActivity)
?.browsingModeManager?.mode == BrowsingMode.Normal ?.browsingModeManager?.mode == BrowsingMode.Normal
val shouldDeleteDataOnQuit = context.components.settings val shouldDeleteDataOnQuit = context.components.settings
.shouldDeleteBrowsingDataOnQuit .shouldDeleteBrowsingDataOnQuit
val syncedTabsInTabsTray = context.components.settings
.syncedTabsInTabsTray
val menuItems = listOfNotNull( val menuItems = listOfNotNull(
downloadsItem, downloadsItem,
historyItem, historyItem,
bookmarksItem, bookmarksItem,
if (syncedTabsInTabsTray) null else syncedTabs, syncedTabs,
settings, settings,
if (shouldDeleteDataOnQuit) deleteDataOnQuit else null, if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
BrowserMenuDivider(), BrowserMenuDivider(),
@ -216,151 +361,146 @@ class DefaultToolbarMenu(
} }
} }
private val settings = BrowserMenuHighlightableItem( private val newCoreMenuItems by lazy {
label = context.getString(R.string.browser_menu_settings), val newTabItem = BrowserMenuImageText(
startImageResource = R.drawable.ic_settings, context.getString(R.string.library_new_tab),
iconTintColorResource = if (hasAccountProblem) R.drawable.ic_bookmark_filled,
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else disabledTextColor()
primaryTextColor(), ) {
textColorResource = if (hasAccountProblem) onItemTapped.invoke(ToolbarMenu.Item.NewTab)
ThemeManager.resolveAttribute(R.attr.primaryText, context) else
primaryTextColor(),
highlight = BrowserMenuHighlight.HighPriority(
endImageResource = R.drawable.ic_sync_disconnected,
backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground),
canPropagate = false
),
isHighlighted = { hasAccountProblem }
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
private val desktopMode = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
initialState = {
selectedSession?.content?.desktopMode ?: false
} }
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
private val addToTopSites = BrowserMenuImageText( val bookmarksItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_top_sites), context.getString(R.string.library_bookmarks),
imageResource = R.drawable.ic_top_sites, R.drawable.ic_bookmark_filled,
iconTintColorResource = primaryTextColor() disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites) onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
} }
private val addToHomescreen = BrowserMenuImageText( val historyItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_add_to_homescreen), context.getString(R.string.library_history),
imageResource = R.drawable.ic_add_to_homescreen, R.drawable.ic_history,
iconTintColorResource = primaryTextColor() disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) onItemTapped.invoke(ToolbarMenu.Item.History)
} }
private val syncedTabs = BrowserMenuImageText( val downloadsItem = BrowserMenuImageText(
label = context.getString(R.string.synced_tabs), context.getString(R.string.library_downloads),
imageResource = R.drawable.ic_synced_tabs, R.drawable.ic_download,
iconTintColorResource = primaryTextColor() disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) onItemTapped.invoke(ToolbarMenu.Item.Downloads)
} }
private val installToHomescreen = BrowserMenuHighlightableItem( val extensionsItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_install_on_homescreen), context.getString(R.string.browser_menu_extensions),
startImageResource = R.drawable.ic_add_to_homescreen, R.drawable.ic_addons_extensions,
iconTintColorResource = primaryTextColor(), disabledTextColor()
highlight = BrowserMenuHighlight.LowPriority( ) {
label = context.getString(R.string.browser_menu_install_on_homescreen), onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
notificationTint = getColor(context, R.color.whats_new_notification_color)
),
isHighlighted = {
!context.settings().installPwaOpened
} }
) {
onItemTapped.invoke(ToolbarMenu.Item.InstallToHomeScreen)
}
private val findInPage = BrowserMenuImageText( val syncedTabsItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page), context.getString(R.string.library_synced_tabs),
imageResource = R.drawable.mozac_ic_search, R.drawable.ic_synced_tabs,
iconTintColorResource = primaryTextColor() disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage) onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
} }
private val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( val findInPageItem = BrowserMenuImageText(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID label = context.getString(R.string.browser_menu_find_in_page),
) imageResource = R.drawable.mozac_ic_search,
iconTintColorResource = disabledTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
}
private val saveToCollection = BrowserMenuImageText( val desktopSiteItem = BrowserMenuImageSwitch(
label = context.getString(R.string.browser_menu_save_to_collection_2), imageResource = R.drawable.ic_desktop,
imageResource = R.drawable.ic_tab_collection, label = context.getString(R.string.browser_menu_desktop_site),
iconTintColorResource = primaryTextColor() initialState = {
) { selectedSession?.content?.desktopMode ?: false
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) }
} ) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
private val deleteDataOnQuit = BrowserMenuImageText( val addToHomeScreenItem = BrowserMenuImageText(
label = context.getString(R.string.delete_browsing_data_on_quit_action), label = context.getString(R.string.browser_menu_add_to_homescreen),
imageResource = R.drawable.ic_exit, imageResource = R.drawable.ic_add_to_homescreen,
iconTintColorResource = primaryTextColor() iconTintColorResource = disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.Quit) onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
} }
private val readerAppearance = BrowserMenuImageText( val addToTopSitesItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_read_appearance), label = context.getString(R.string.browser_menu_add_to_top_sites),
imageResource = R.drawable.ic_readermode_appearance, imageResource = R.drawable.ic_top_sites,
iconTintColorResource = primaryTextColor() iconTintColorResource = disabledTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.ReaderModeAppearance) onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
} }
private val openInApp = BrowserMenuHighlightableItem( val saveToCollectionItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_open_app_link), label = context.getString(R.string.browser_menu_save_to_collection_2),
startImageResource = R.drawable.ic_open_in_app, imageResource = R.drawable.ic_tab_collection,
iconTintColorResource = primaryTextColor(), iconTintColorResource = disabledTextColor()
highlight = BrowserMenuHighlight.LowPriority( ) {
label = context.getString(R.string.browser_menu_open_app_link), onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection)
notificationTint = getColor(context, R.color.whats_new_notification_color) }
),
isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val historyItem = BrowserMenuImageText( val settingsItem = BrowserMenuHighlightableItem(
context.getString(R.string.library_history), label = context.getString(R.string.browser_menu_settings),
R.drawable.ic_history, startImageResource = R.drawable.ic_settings,
primaryTextColor() iconTintColorResource = disabledTextColor(),
) { textColorResource = if (hasAccountProblem)
onItemTapped.invoke(ToolbarMenu.Item.History) ThemeManager.resolveAttribute(R.attr.primaryText, context) else
} primaryTextColor(),
highlight = BrowserMenuHighlight.HighPriority(
endImageResource = R.drawable.ic_sync_disconnected,
backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground),
canPropagate = false
),
isHighlighted = { hasAccountProblem }
) {
onItemTapped.invoke(ToolbarMenu.Item.Settings)
}
val bookmarksItem = BrowserMenuImageText( val menuItems = listOfNotNull(
context.getString(R.string.library_bookmarks), newTabItem,
R.drawable.ic_bookmark_filled, BrowserMenuDivider(),
primaryTextColor() bookmarksItem,
) { historyItem,
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) downloadsItem,
} extensionsItem,
syncedTabsItem,
BrowserMenuDivider(),
findInPageItem,
desktopSiteItem,
BrowserMenuDivider(),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
addToTopSitesItem,
saveToCollectionItem,
BrowserMenuDivider(),
settingsItem,
BrowserMenuDivider(),
menuToolbar
)
val downloadsItem = BrowserMenuImageText( menuItems
context.getString(R.string.library_downloads),
R.drawable.ic_download,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
} }
@ColorRes @ColorRes
@VisibleForTesting @VisibleForTesting
internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
@ColorRes
@VisibleForTesting
internal fun disabledTextColor() = R.color.toolbar_menu_transparent
@VisibleForTesting @VisibleForTesting
internal fun registerForIsBookmarkedUpdates() { internal fun registerForIsBookmarkedUpdates() {
store.flowScoped(lifecycleOwner) { flow -> store.flowScoped(lifecycleOwner) { flow ->

@ -1,50 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.components.toolbar
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.appbar.AppBarLayout
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.concept.engine.EngineView
import mozilla.components.concept.engine.EngineView.InputResult.INPUT_RESULT_UNHANDLED
import org.mozilla.fenix.ext.settings
/**
* ScrollingViewBehavior that will setScrollFlags on BrowserToolbar based on EngineView touch handling
*/
@ExperimentalCoroutinesApi
class SwipeRefreshScrollingViewBehavior(
context: Context,
attrs: AttributeSet?,
private val engineView: EngineView,
private val browserToolbarView: BrowserToolbarView
) : AppBarLayout.ScrollingViewBehavior(context, attrs) {
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
child: View,
directTargetChild: View,
target: View,
axes: Int,
type: Int
): Boolean {
if (!browserToolbarView.view.context.settings().shouldUseBottomToolbar) {
val shouldDisable = engineView.getInputResult() == INPUT_RESULT_UNHANDLED
browserToolbarView.setScrollFlags(shouldDisable)
}
return super.onStartNestedScroll(
coordinatorLayout,
child,
directTargetChild,
target,
axes,
type
)
}
}

@ -21,7 +21,6 @@ import mozilla.components.feature.tabs.toolbar.TabCounterToolbarButton
import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature import mozilla.components.feature.toolbar.ToolbarAutocompleteFeature
import mozilla.components.feature.toolbar.ToolbarFeature import mozilla.components.feature.toolbar.ToolbarFeature
import mozilla.components.feature.toolbar.ToolbarPresenter import mozilla.components.feature.toolbar.ToolbarPresenter
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -45,7 +44,7 @@ abstract class ToolbarIntegration(
store, store,
sessionId, sessionId,
ToolbarFeature.UrlRenderConfiguration( ToolbarFeature.UrlRenderConfiguration(
PublicSuffixList(context), context.components.publicSuffixList,
ThemeManager.resolveAttribute(R.attr.primaryText, context), ThemeManager.resolveAttribute(R.attr.primaryText, context),
renderStyle = renderStyle renderStyle = renderStyle
) )

@ -31,6 +31,7 @@ interface ToolbarMenu {
object Bookmarks : Item() object Bookmarks : Item()
object History : Item() object History : Item()
object Downloads : Item() object Downloads : Item()
object NewTab : Item()
} }
val menuBuilder: BrowserMenuBuilder val menuBuilder: BrowserMenuBuilder

@ -6,7 +6,6 @@ package org.mozilla.fenix.customtabs
import android.app.Activity import android.app.Activity
import androidx.appcompat.content.res.AppCompatResources.getDrawable import androidx.appcompat.content.res.AppCompatResources.getDrawable
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
@ -19,7 +18,6 @@ import org.mozilla.fenix.components.toolbar.ToolbarMenu
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
class CustomTabsIntegration( class CustomTabsIntegration(
sessionManager: SessionManager,
store: BrowserStore, store: BrowserStore,
useCases: CustomTabsUseCases, useCases: CustomTabsUseCases,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
@ -61,16 +59,6 @@ class CustomTabsIntegration(
// If in private mode, override toolbar background to use private color // If in private mode, override toolbar background to use private color
// See #5334 // See #5334
if (isPrivate) { if (isPrivate) {
sessionManager.findSessionById(sessionId)?.apply {
val config = customTabConfig
customTabConfig = config?.copy(
// Don't set toolbar background automatically
toolbarColor = null,
// Force tinting the action button
actionButtonConfig = config.actionButtonConfig?.copy(tint = true)
)
}
toolbar.background = getDrawable(activity, R.drawable.toolbar_background) toolbar.background = getDrawable(activity, R.drawable.toolbar_background)
} }
} }
@ -94,7 +82,9 @@ class CustomTabsIntegration(
menuItemIndex = START_OF_MENU_ITEMS_INDEX, menuItemIndex = START_OF_MENU_ITEMS_INDEX,
window = activity.window, window = activity.window,
shareListener = { onItemTapped.invoke(ToolbarMenu.Item.Share) }, shareListener = { onItemTapped.invoke(ToolbarMenu.Item.Share) },
closeListener = { activity.finishAndRemoveTask() } closeListener = { activity.finishAndRemoveTask() },
updateToolbarBackground = !isPrivate,
forceActionButtonTinting = isPrivate
) )
override fun start() = feature.start() override fun start() = feature.start()

@ -61,7 +61,6 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
customTabsIntegration.set( customTabsIntegration.set(
feature = CustomTabsIntegration( feature = CustomTabsIntegration(
sessionManager = requireComponents.core.sessionManager,
store = requireComponents.core.store, store = requireComponents.core.store,
useCases = requireComponents.useCases.customTabsUseCases, useCases = requireComponents.useCases.customTabsUseCases,
toolbar = toolbar, toolbar = toolbar,
@ -139,7 +138,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
customTabSessionId, customTabSessionId,
manifest, manifest,
WebAppSiteControlsBuilder( WebAppSiteControlsBuilder(
requireComponents.core.sessionManager, requireComponents.core.store,
requireComponents.useCases.sessionUseCases.reload, requireComponents.useCases.sessionUseCases.reload,
customTabSessionId, customTabSessionId,
manifest manifest

@ -7,15 +7,16 @@ package org.mozilla.fenix.customtabs
import android.app.Notification import android.app.Notification
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.CustomTabSessionState import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.manifest.WebAppManifest import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.feature.pwa.feature.SiteControlsBuilder import mozilla.components.feature.pwa.feature.SiteControlsBuilder
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import org.mozilla.fenix.R import org.mozilla.fenix.R
class WebAppSiteControlsBuilder( class WebAppSiteControlsBuilder(
private val sessionManager: SessionManager, private val store: BrowserStore,
reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase, reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val sessionId: String, private val sessionId: String,
private val manifest: WebAppManifest private val manifest: WebAppManifest
@ -26,9 +27,9 @@ class WebAppSiteControlsBuilder(
override fun buildNotification(context: Context, builder: Notification.Builder) { override fun buildNotification(context: Context, builder: Notification.Builder) {
inner.buildNotification(context, builder) inner.buildNotification(context, builder)
val isPrivateSession = sessionManager.findSessionById(sessionId)?.private ?: false if (store.state.findCustomTab(sessionId)?.content?.private != true) {
return
if (!isPrivateSession) { return } }
builder.setSmallIcon(R.drawable.ic_private_browsing) builder.setSmallIcon(R.drawable.ic_private_browsing)
builder.setContentTitle(context.getString(R.string.pwa_site_controls_title_private, manifest.name)) builder.setContentTitle(context.getString(R.string.pwa_site_controls_title_private, manifest.name))

@ -6,8 +6,9 @@ package org.mozilla.fenix.experiments
class Experiments { class Experiments {
companion object { companion object {
const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation" const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation-v3"
const val BOOKMARK_ICON = "fenix-bookmark-list-icon" const val BOOKMARK_ICON = "fenix-bookmark-list-icon"
const val ANDROID_KEYSTORE = "fenix-android-keystore"
} }
} }
@ -15,7 +16,7 @@ class ExperimentBranch {
companion object { companion object {
const val TREATMENT = "treatment" const val TREATMENT = "treatment"
const val CONTROL = "control" const val CONTROL = "control"
const val A1 = "A1" const val A1 = "a1"
const val A2 = "A2" const val A2 = "a2"
} }
} }

@ -0,0 +1,72 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.experiments
import android.content.Context
import android.net.Uri
import android.os.StrictMode
import io.sentry.Sentry
import mozilla.components.service.nimbus.NimbusApi
import mozilla.components.service.nimbus.Nimbus
import mozilla.components.service.nimbus.NimbusDisabled
import mozilla.components.service.nimbus.NimbusServerSettings
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
@Suppress("TooGenericExceptionCaught")
fun createNimbus(context: Context, url: String?): NimbusApi =
try {
// Eventually we'll want to use `NimbusDisabled` when we have no NIMBUS_ENDPOINT.
// but we keep this here to not mix feature flags and how we configure Nimbus.
val serverSettings = if (!url.isNullOrBlank()) {
NimbusServerSettings(url = Uri.parse(url))
} else {
null
}
// Global opt out state is stored in Nimbus, and shouldn't be toggled to `true`
// from the app unless the user does so from a UI control.
// However, the user may have opt-ed out of mako experiments already, so
// we should respect that setting here.
val enabled =
context.components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
context.settings().isExperimentationEnabled
}
Nimbus(context, serverSettings).apply {
// This performs the minimal amount of work required to load branch and enrolment data
// into memory. If `getExperimentBranch` is called from another thread between here
// and the next nimbus disk write (setting `globalUserParticipation` or
// `applyPendingExperiments()`) then this has you covered.
// This call does its work on the db thread.
initialize()
if (!enabled) {
// This opts out of nimbus experiments. It involves writing to disk, so does its
// work on the db thread.
globalUserParticipation = enabled
}
// We may have downloaded experiments on a previous run, so let's start using them
// now. We didn't do this earlier, so as to make getExperimentBranch and friends returns
// the same thing throughout the session. This call does its work on the db thread.
applyPendingExperiments()
// Now fetch the experiments from the server. These will be available for feature
// configuration on the next run of the app. This call launches on the fetch thread.
fetchExperiments()
}
} catch (e: Throwable) {
// Something went wrong. We'd like not to, but stability of the app is more important than
// failing fast here.
if (isSentryEnabled()) {
Sentry.capture(e)
} else {
Logger.error("Failed to initialize Nimbus", e)
}
NimbusDisabled()
}

@ -15,6 +15,6 @@ val Context.bookmarkStorage: PlacesBookmarksStorage
* Removes [children] from [BookmarkNode.children] and returns the new modified [BookmarkNode]. * Removes [children] from [BookmarkNode.children] and returns the new modified [BookmarkNode].
*/ */
operator fun BookmarkNode.minus(children: Set<BookmarkNode>): BookmarkNode { operator fun BookmarkNode.minus(children: Set<BookmarkNode>): BookmarkNode {
val removedChildrenGuids = children.map { it.guid }.toSet() val removedChildrenGuids = children.map { it.guid }
return this.copy(children = this.children?.filterNot { removedChildrenGuids.contains(it.guid) }) return this.copy(children = this.children?.filterNot { removedChildrenGuids.contains(it.guid) })
} }

@ -1,16 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ext
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.MediaState
fun BrowserState.getMediaStateForSession(sessionId: String): MediaState.State {
return if (media.aggregate.activeTabId == sessionId) {
media.aggregate.state
} else {
MediaState.State.NONE
}
}

@ -7,11 +7,11 @@ package org.mozilla.fenix.home
import android.animation.Animator import android.animation.Animator
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.res.Configuration
import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.os.StrictMode import android.os.StrictMode
import android.view.Display.FLAG_SECURE
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -84,6 +84,7 @@ import mozilla.components.support.ktx.android.content.res.resolveAttribute
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import mozilla.components.ui.tabcounter.TabCounterMenu import mozilla.components.ui.tabcounter.TabCounterMenu
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
@ -103,8 +104,9 @@ import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.mozonline.showPrivacyPopWindow
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView import org.mozilla.fenix.home.sessioncontrol.SessionControlView
@ -166,6 +168,9 @@ class HomeFragment : Fragment() {
private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>() private val topSitesFeature = ViewBoundFeatureWrapper<TopSitesFeature>()
@VisibleForTesting
internal var getMenuButton: () -> MenuButton? = { menuButton }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -175,6 +180,12 @@ class HomeFragment : Fragment() {
requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun) requireComponents.analytics.metrics.track(Event.OpenedAppFirstRun)
} }
} }
if (!onboarding.userHasBeenOnboarded() &&
requireContext().settings().shouldShowPrivacyPopWindow &&
Config.channel.isMozillaOnline) {
showPrivacyPopWindow(requireContext(), requireActivity())
}
} }
@Suppress("LongMethod") @Suppress("LongMethod")
@ -223,7 +234,7 @@ class HomeFragment : Fragment() {
storage = components.core.topSitesStorage, storage = components.core.topSitesStorage,
config = ::getTopSitesConfig config = ::getTopSitesConfig
), ),
owner = this, owner = viewLifecycleOwner,
view = view view = view
) )
@ -266,6 +277,12 @@ class HomeFragment : Fragment() {
return view return view
} }
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
getMenuButton()?.dismissMenu()
}
private fun dismissTip(tip: Tip) { private fun dismissTip(tip: Tip) {
sessionControlInteractor.onCloseTip(tip) sessionControlInteractor.onCloseTip(tip)
} }
@ -389,12 +406,6 @@ class HomeFragment : Fragment() {
} }
} }
if (browsingModeManager.mode.isPrivate) {
requireActivity().window.addFlags(FLAG_SECURE)
} else {
requireActivity().window.clearFlags(FLAG_SECURE)
}
consumeFrom(requireComponents.core.store) { consumeFrom(requireComponents.core.store) {
updateTabCounter(it) updateTabCounter(it)
} }
@ -533,7 +544,6 @@ class HomeFragment : Fragment() {
sessionControlView = null sessionControlView = null
appBarLayout = null appBarLayout = null
bundleArgs.clear() bundleArgs.clear()
requireActivity().window.clearFlags(FLAG_SECURE)
} }
override fun onStart() { override fun onStart() {

@ -5,7 +5,6 @@
package org.mozilla.fenix.home package org.mozilla.fenix.home
import android.graphics.Bitmap import android.graphics.Bitmap
import mozilla.components.browser.state.state.MediaState
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
@ -28,8 +27,7 @@ data class Tab(
val hostname: String, val hostname: String,
val title: String, val title: String,
val selected: Boolean? = null, val selected: Boolean? = null,
val icon: Bitmap? = null, val icon: Bitmap? = null
val mediaState: MediaState.State
) )
/** /**

@ -190,7 +190,7 @@ class HomeMenu(
if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null, if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null,
settingsItem, settingsItem,
BrowserMenuDivider(), BrowserMenuDivider(),
if (settings.syncedTabsInTabsTray) null else syncedTabsItem, syncedTabsItem,
bookmarksItem, bookmarksItem,
historyItem, historyItem,
downloadsItem, downloadsItem,

@ -7,10 +7,8 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -44,17 +42,9 @@ class OpenSpecificTabIntentProcessor(
} }
private fun getAction(): String { private fun getAction(): String {
return if (newMediaSessionApi) { return AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
} else {
AbstractMediaService.Companion.ACTION_SWITCH_TAB
}
} }
private fun getTabId(): String { private fun getTabId(): String {
return if (newMediaSessionApi) { return AbstractMediaSessionService.Companion.EXTRA_TAB_ID
AbstractMediaSessionService.Companion.EXTRA_TAB_ID
} else {
AbstractMediaService.Companion.EXTRA_TAB_ID
}
} }

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.mozonline
import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.AttributeSet
import android.view.View
import android.widget.ImageButton
import mozilla.components.concept.engine.EngineSession
import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.contextmenu.DefaultSelectionActionDelegate
import mozilla.components.feature.search.BrowserStoreSearchAdapter
import mozilla.components.support.ktx.android.content.call
import mozilla.components.support.ktx.android.content.email
import mozilla.components.support.ktx.android.content.share
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
/**
* A special activity for displaying the detail content about privacy hyperlinked in alert dialog.
*/
class PrivacyContentDisplayActivity : Activity(), EngineSession.Observer {
private lateinit var engineView: EngineView
private lateinit var closeButton: ImageButton
private lateinit var engineSession: EngineSession
private var url: String? = ""
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_privacy_content_display)
val addr = intent.extras
if (addr != null) {
url = addr.getString("url")
}
engineView = findViewById<View>(R.id.privacyContentEngineView) as EngineView
closeButton = findViewById<View>(R.id.privacyContentCloseButton) as ImageButton
engineSession = components.core.engine.createSession()
}
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet
): View? = when (name) {
EngineView::class.java.name -> components.core.engine.createView(context, attrs).apply {
selectionActionDelegate = DefaultSelectionActionDelegate(
BrowserStoreSearchAdapter(
components.core.store
),
resources = context.resources,
shareTextClicked = { share(it) },
emailTextClicked = { email(it) },
callTextClicked = { call(it) }
)
}.asView()
else -> super.onCreateView(parent, name, context, attrs)
}
override fun onStart() {
super.onStart()
engineSession.register(this)
engineSession.let { engineSession ->
engineView.render(engineSession)
url?.let { engineSession.loadUrl(it) }
}
closeButton.setOnClickListener { finish() }
}
override fun onStop() {
super.onStop()
engineSession.unregister(this)
}
override fun onDestroy() {
super.onDestroy()
engineSession.close()
}
}

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.mozonline
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.text.SpannableString
import android.text.Spanned
import android.text.method.LinkMovementMethod
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import kotlin.system.exitProcess
fun showPrivacyPopWindow(context: Context, activity: Activity) {
val content = context.getString(R.string.privacy_notice_content)
// Use hyperlinks to display details about privacy
val messageClickable1 = context.getString(R.string.privacy_notice_clickable1)
val messageClickable2 = context.getString(R.string.privacy_notice_clickable2)
val messageClickable3 = context.getString(R.string.privacy_notice_clickable3)
val messageSpannable = SpannableString(content)
val clickableSpan1 = PrivacyContentSpan(Position.POS1, context)
val clickableSpan2 = PrivacyContentSpan(Position.POS2, context)
val clickableSpan3 = PrivacyContentSpan(Position.POS3, context)
messageSpannable.setSpan(clickableSpan1, content.indexOf(messageClickable1),
content.indexOf(messageClickable1) + messageClickable1.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
messageSpannable.setSpan(clickableSpan2, content.indexOf(messageClickable2),
content.indexOf(messageClickable2) + messageClickable2.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
messageSpannable.setSpan(clickableSpan3, content.indexOf(messageClickable3),
content.indexOf(messageClickable3) + messageClickable3.length, Spanned.SPAN_INCLUSIVE_INCLUSIVE)
// Users can only use fenix after they agree with the privacy notice
val builder = AlertDialog.Builder(activity)
.setPositiveButton(context.getString(R.string.privacy_notice_positive_button),
DialogInterface.OnClickListener { _, _ ->
context.settings().shouldShowPrivacyPopWindow = false
})
.setNeutralButton(context.getString(R.string.privacy_notice_neutral_button),
DialogInterface.OnClickListener { _, _ -> exitProcess(0) })
.setTitle(context.getString(R.string.privacy_notice_title))
.setMessage(messageSpannable)
.setCancelable(false)
val alertDialog: AlertDialog = builder.create()
alertDialog.show()
alertDialog.findViewById<TextView>(android.R.id.message)?.movementMethod = LinkMovementMethod.getInstance()
}

@ -0,0 +1,44 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home.mozonline
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.style.ClickableSpan
import android.view.View
object Position {
const val POS1 = 1
const val POS2 = 2
const val POS3 = 3
}
object ADDR {
const val URL1 = "https://www.mozilla.org/en-US/MPL/"
const val URL2 = "https://www.mozilla.org/en-US/foundation/trademarks/policy/"
const val URL3 = "https://www.mozilla.org/zh-CN/privacy/firefox/"
}
class PrivacyContentSpan(var pos: Int, var context: Context) :
ClickableSpan() {
override fun onClick(widget: View) {
/**
* To avoid users directly using fenix by clicking these urls before
* they click positive button of privacy notice alert dialog, start
* PrivacyContentDisplayActivity to display them.
*/
val engineViewIntent = Intent(context, PrivacyContentDisplayActivity::class.java)
engineViewIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val addr = Bundle()
when (pos) {
Position.POS1 -> addr.putString("url", ADDR.URL1)
Position.POS2 -> addr.putString("url", ADDR.URL2)
Position.POS3 -> addr.putString("url", ADDR.URL3)
}
engineViewIntent.putExtras(addr)
context.startActivity(engineViewIntent)
}
}

@ -21,23 +21,25 @@ import org.mozilla.fenix.library.bookmarks.viewholders.BookmarkSeparatorViewHold
class BookmarkAdapter(private val emptyView: View, private val interactor: BookmarkViewInteractor) : class BookmarkAdapter(private val emptyView: View, private val interactor: BookmarkViewInteractor) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var tree: List<BookmarkNode> = listOf() @VisibleForTesting var tree: List<BookmarkNode> = listOf()
private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal() private var mode: BookmarkFragmentState.Mode = BookmarkFragmentState.Mode.Normal()
private var isFirstRun = true private var isFirstRun = true
fun updateData(tree: BookmarkNode?, mode: BookmarkFragmentState.Mode) { fun updateData(tree: BookmarkNode?, mode: BookmarkFragmentState.Mode) {
// Display folders above all other bookmarks.
val allNodes = tree?.children.orEmpty() val allNodes = tree?.children.orEmpty()
val folders: MutableList<BookmarkNode> = mutableListOf() val folders: MutableList<BookmarkNode> = mutableListOf()
val notFolders: MutableList<BookmarkNode> = mutableListOf() val notFolders: MutableList<BookmarkNode> = mutableListOf()
val separators: MutableList<BookmarkNode> = mutableListOf()
allNodes.forEach { allNodes.forEach {
if (it.type == BookmarkNodeType.FOLDER) { when (it.type) {
folders.add(it) BookmarkNodeType.SEPARATOR -> separators.add(it)
} else { BookmarkNodeType.FOLDER -> folders.add(it)
notFolders.add(it) else -> notFolders.add(it)
} }
} }
val newTree = folders + notFolders // Display folders above all other bookmarks. Exclude separators.
// For separator removal, see discussion in https://github.com/mozilla-mobile/fenix/issues/15214
val newTree = folders + notFolders - separators
val diffUtil = DiffUtil.calculateDiff( val diffUtil = DiffUtil.calculateDiff(
BookmarkDiffUtil( BookmarkDiffUtil(

@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.BrowserState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -50,6 +51,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
private lateinit var metrics: MetricController private lateinit var metrics: MetricController
private var undoScope: CoroutineScope? = null private var undoScope: CoroutineScope? = null
private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null
private lateinit var downloadsUseCases: DownloadsUseCases
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -59,6 +61,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
val view = inflater.inflate(R.layout.fragment_downloads, container, false) val view = inflater.inflate(R.layout.fragment_downloads, container, false)
val items = provideDownloads(requireComponents.core.store.state) val items = provideDownloads(requireComponents.core.store.state)
downloadsUseCases = requireContext().components.useCases.downloadUseCases
downloadStore = StoreProvider.get(this) { downloadStore = StoreProvider.get(this) {
DownloadFragmentStore( DownloadFragmentStore(
@ -85,6 +88,10 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
return view return view
} }
/**
* Returns a list of available downloads to be displayed to the user.
* Downloads must be COMPLETED and existent on disk.
*/
@VisibleForTesting @VisibleForTesting
internal fun provideDownloads(state: BrowserState): List<DownloadItem> { internal fun provideDownloads(state: BrowserState): List<DownloadItem> {
return state.downloads.values return state.downloads.values
@ -128,9 +135,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ -> setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
// Use fragment's lifecycle; the view may be gone by the time dialog is interacted with. // Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
lifecycleScope.launch(IO) { lifecycleScope.launch(IO) {
context.let { downloadsUseCases.removeAllDownloads()
it.components.useCases.downloadUseCases.removeAllDownloads()
}
updatePendingDownloadToDelete(downloadStore.state.items.toSet()) updatePendingDownloadToDelete(downloadStore.state.items.toSet())
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
showSnackBar( showSnackBar(
@ -146,6 +151,11 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
} }
} }
/**
* Schedules [items] for deletion.
* Note: When tapping on a download item's "trash" button
* (itemView.overflow_menu) this [items].size() will be 1.
*/
private fun deleteDownloadItems(items: Set<DownloadItem>) { private fun deleteDownloadItems(items: Set<DownloadItem>) {
metrics.track(Event.DownloadsItemDeleted) metrics.track(Event.DownloadsItemDeleted)
@ -155,10 +165,10 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
requireView(), requireView(),
getMultiSelectSnackBarMessage(items), getMultiSelectSnackBarMessage(items),
getString(R.string.bookmark_undo_deletion), getString(R.string.bookmark_undo_deletion),
{ onCancel = {
undoPendingDeletion(items) undoPendingDeletion(items)
}, },
getDeleteDownloadItemsOperation(items) operation = getDeleteDownloadItemsOperation(downloadsUseCases, items)
) )
} }
@ -210,6 +220,9 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
else -> super.onOptionsItemSelected(item) else -> super.onOptionsItemSelected(item)
} }
/**
* Provides a message to the Undo snackbar.
*/
private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String { private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String {
return if (downloadItems.size > 1) { return if (downloadItems.size > 1) {
getString(R.string.download_delete_multiple_items_snackbar_1) getString(R.string.download_delete_multiple_items_snackbar_1)
@ -246,14 +259,18 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
metrics.track(Event.DownloadsItemOpened) metrics.track(Event.DownloadsItemOpened)
} }
private fun getDeleteDownloadItemsOperation(items: Set<DownloadItem>): (suspend () -> Unit) { /**
* Launches the coroutine to delete the provided [items].
*/
private fun getDeleteDownloadItemsOperation(
downloadUseCases: DownloadsUseCases,
items: Set<DownloadItem>
): (suspend () -> Unit) {
return { return {
CoroutineScope(IO).launch { CoroutineScope(IO).launch {
downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode) downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode)
context?.let { for (item in items) {
for (item in items) { downloadUseCases.removeDownload(item.id)
it.components.useCases.downloadUseCases.removeDownload(item.id)
}
} }
downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode) downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode)
pendingDownloadDeletionJob = null pendingDownloadDeletionJob = null
@ -261,8 +278,14 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
} }
} }
/**
* Queues the [getDeleteDownloadItemsOperation] job in [pendingDownloadDeletionJob] in case
* the user exits the fragment and we need to quickly execute the queued deletion.
* And adds the [items] to be deleted to the list of [DownloadFragmentStore.pendingDeletionIds],
* which is used to determine what items to show and what items to hide from the user.
*/
private fun updatePendingDownloadToDelete(items: Set<DownloadItem>) { private fun updatePendingDownloadToDelete(items: Set<DownloadItem>) {
pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(items) pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(downloadsUseCases, items)
val ids = items.map { item -> item.id }.toSet() val ids = items.map { item -> item.id }.toSet()
downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids)) downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids))
} }
@ -273,6 +296,9 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids)) downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids))
} }
/**
* Executes pending job(s) when leaving [DownloadFragment].
*/
private fun invokePendingDeletion() { private fun invokePendingDeletion() {
pendingDownloadDeletionJob?.let { pendingDownloadDeletionJob?.let {
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycleScope.launch {

@ -1,16 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.media
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.media.service.AbstractMediaService
import org.mozilla.fenix.ext.components
/**
* [AbstractMediaService] implementation for injecting [BrowserStore] singleton.
*/
class MediaService : AbstractMediaService() {
override val store: BrowserStore by lazy { components.core.store }
}

@ -0,0 +1,82 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.perf
import android.os.Handler
import android.os.Looper
import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import mozilla.components.concept.base.profiler.Profiler
import mozilla.components.support.base.facts.Action
import mozilla.components.support.base.facts.Fact
import mozilla.components.support.base.facts.FactProcessor
/**
* A fact processor that adds Gecko profiler markers for [Fact]s matching a specific format.
* We look for the following format:
* ```
* Fact(
* action = Action.IMPLEMENTATION_DETAIL
* item = <marker name>
* )
* ```
*
* This allows us to add profiler markers from android-components code. Using the Fact API for this
* purpose, rather than calling [Profiler.addMarker] directly inside components, has trade-offs. Its
* downsides are that it is less explicit and tooling does not work as well on it. However, we felt
* it was worthwhile because:
*
* 1. we don't know what profiler markers are useful so we want to be able to iterate quickly.
* Adding dependencies on the Profiler and landing these changes across two repos hinders that
* 2. we want to instrument the code as close to specific method calls as possible (e.g.
* GeckoSession.loadUrl) but it's not always easy to do so (e.g. in the previous example, passing a
* Profiler reference to GeckoEngineSession is difficult because GES is not a global dependency)
* 3. we can only add Profiler markers from the main thread so adding markers will become more
* difficult if we have to understand the threading needs of each Profiler call site
*
* An additional benefit with having this infrastructure is that it's easy to add Profiler markers
* for local debugging.
*
* That being said, if we find a location where it would be valuable to have a long term Profiler
* marker, we should consider instrumenting it via the [Profiler] API.
*/
class ProfilerMarkerFactProcessor @VisibleForTesting(otherwise = PRIVATE) constructor(
// We use a provider to defer accessing the profiler until we need it, because the property is a
// child of the engine property and we don't want to initialize it earlier than we intend to.
private val profilerProvider: () -> Profiler?,
private val mainHandler: Handler = Handler(Looper.getMainLooper()),
private val getMyLooper: () -> Looper? = { Looper.myLooper() }
) : FactProcessor {
override fun process(fact: Fact) {
if (fact.action != Action.IMPLEMENTATION_DETAIL) {
return
}
val markerName = fact.item
// Java profiler markers can only be added from the main thread so, for now, we push all
// markers to the the main thread (which also groups all the markers together,
// making it easier to read).
val profiler = profilerProvider()
if (getMyLooper() == mainHandler.looper) {
profiler?.addMarker(markerName)
} else {
// To reduce the performance burden, we could early return if the profiler isn't active.
// However, this would change the performance characteristics from when the profiler is
// active and when it's inactive so we always post instead.
val now = profiler?.getProfilerTime()
mainHandler.post {
// We set now to both start and end time because we want a marker of without duration
// and if end is omitted, the duration is created implicitly.
profiler?.addMarker(markerName, now, now, null)
}
}
}
companion object {
fun create(profilerProvider: () -> Profiler?) = ProfilerMarkerFactProcessor(profilerProvider)
}
}

@ -0,0 +1,22 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.perf
import android.view.View
import androidx.core.view.doOnPreDraw
import mozilla.components.concept.base.profiler.Profiler
/**
* A container for functions for when adding a profiler marker is less readable
* (e.g. multiple lines, more advanced logic).
*/
object ProfilerMarkers {
fun homeActivityOnStart(rootContainer: View, profiler: Profiler?) {
rootContainer.doOnPreDraw {
profiler?.addMarker("onPreDraw", "expected first frame via HomeActivity.onStart")
}
}
}

@ -54,20 +54,30 @@ internal class StartupFrameworkStartMeasurement(
if (applicationInitNanos < 0) { if (applicationInitNanos < 0) {
telemetry.frameworkStartError.set(true) telemetry.frameworkStartError.set(true)
} else { } else {
val clockTicksPerSecond = stat.clockTicksPerSecond.also {
// framework* is derived from the number of clock ticks per second. To ensure this
// value does not throw off our result, we capture it too.
telemetry.clockTicksPerSecond.add(it.toInt())
}
// In our brief analysis, clock ticks per second was overwhelmingly equal to 100. To make
// analysis easier in GLAM, we split the results into two separate metrics. See the
// metric descriptions for more details.
@Suppress("MagicNumber") // it's more confusing to separate the comment above from the value declaration.
val durationMetric =
if (clockTicksPerSecond == 100L) telemetry.frameworkPrimary else telemetry.frameworkSecondary
try { try {
telemetry.frameworkStart.setRawNanos(getFrameworkStartNanos()) durationMetric.setRawNanos(getFrameworkStartNanos())
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
// Privacy managers can add hooks that block access to reading system /proc files. // Privacy managers can add hooks that block access to reading system /proc files.
// We want to catch these exception and report an error on accessing the file // We want to catch these exception and report an error on accessing the file
// rather than an implementation error. // rather than an implementation error.
telemetry.frameworkStartReadError.set(true) telemetry.frameworkStartReadError.set(true)
} }
// frameworkStart is derived from the number of clock ticks per second. To ensure this
// value does not throw off our result, we capture it too.
telemetry.clockTicksPerSecond.add(stat.clockTicksPerSecond.toInt())
} }
} }
/** /**
* @throws [java.io.FileNotFoundException] * @throws [java.io.FileNotFoundException]
*/ */

@ -10,6 +10,7 @@
package org.mozilla.fenix.perf package org.mozilla.fenix.perf
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.StrictMode import android.os.StrictMode
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -19,11 +20,12 @@ import androidx.fragment.app.FragmentManager
import mozilla.components.support.ktx.android.os.resetAfter import mozilla.components.support.ktx.android.os.resetAfter
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.utils.ManufacturerCodes
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import java.util.concurrent.Executors
import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.atomic.AtomicLong
private const val MANUFACTURE_HUAWEI: String = "HUAWEI" private const val DELAY_TO_REMOVE_STRICT_MODE_MILLIS = 1000L
private const val MANUFACTURE_ONE_PLUS: String = "OnePlus"
private val logger = Performance.logger private val logger = Performance.logger
private val mainLooper = Looper.getMainLooper() private val mainLooper = Looper.getMainLooper()
@ -65,8 +67,8 @@ class StrictModeManager(
val threadPolicy = StrictMode.ThreadPolicy.Builder() val threadPolicy = StrictMode.ThreadPolicy.Builder()
.detectAll() .detectAll()
.penaltyLog() .penaltyLog()
if (setPenaltyDeath && Build.MANUFACTURER !in strictModeExceptionList) { if (setPenaltyDeath) {
threadPolicy.penaltyDeath() threadPolicy.penaltyDeathWithIgnores()
} }
StrictMode.setThreadPolicy(threadPolicy.build()) StrictMode.setThreadPolicy(threadPolicy.build())
@ -96,10 +98,18 @@ class StrictModeManager(
fragmentManager.registerFragmentLifecycleCallbacks(object : fragmentManager.registerFragmentLifecycleCallbacks(object :
FragmentManager.FragmentLifecycleCallbacks() { FragmentManager.FragmentLifecycleCallbacks() {
override fun onFragmentResumed(fm: FragmentManager, f: Fragment) { override fun onFragmentResumed(fm: FragmentManager, f: Fragment) {
enableStrictMode(setPenaltyDeath = false)
fm.unregisterFragmentLifecycleCallbacks(this) fm.unregisterFragmentLifecycleCallbacks(this)
}
}, false) // If we don't post when using penaltyListener on P+, the violation listener is never
// called. My best guess is that, unlike penaltyDeath, the violations are not
// delivered instantaneously so posting gives time for the violation listeners to
// run before they are removed here. This may be a race so we give the listeners a
// little extra time to run too though this way we may accidentally trigger
// violations for non-startup, which we haven't planned to do yet.
Handler(mainLooper).postDelayed({
enableStrictMode(setPenaltyDeath = false)
}, DELAY_TO_REMOVE_STRICT_MODE_MILLIS)
} }, false)
} }
/** /**
@ -137,13 +147,35 @@ class StrictModeManager(
functionBlock() functionBlock()
} }
} }
}
/** /**
* There are certain manufacturers that have custom font classes for the OS systems. * There are certain manufacturers that have custom font classes for the OS systems.
* These classes violates the [StrictMode] policies on startup. As a workaround, we create * These classes violates the [StrictMode] policies on startup. As a workaround, we create
* an exception list for these manufacturers so that dialogs do not show up on start up. * an exception list for these manufacturers so that dialogs do not show up on start up.
* To add a new manufacturer to the list, log "Build.MANUFACTURER" from the device to get the * To add a new manufacturer to the list, log "Build.MANUFACTURER" from the device to get the
* exact name of the manufacturer. * exact name of the manufacturer.
*/ */
private val strictModeExceptionList = setOf(MANUFACTURE_HUAWEI, MANUFACTURE_ONE_PLUS) private val strictModeExceptionList = setOf(ManufacturerCodes.HUAWEI, ManufacturerCodes.ONE_PLUS)
private fun StrictMode.ThreadPolicy.Builder.penaltyDeathWithIgnores(): StrictMode.ThreadPolicy.Builder {
// This workaround was added before we realized we can ignored based on violation contents
// (see code below). This solution - blanket disabling StrictMode on some manufacturers - isn't
// great so, if we have time, we should consider reimplementing these fixes using the methods below.
if (Build.MANUFACTURER in strictModeExceptionList) {
return this
}
// If we want to apply ignores based on stack trace contents to APIs below P, we can use this methodology:
// https://medium.com/@tokudu/how-to-whitelist-strictmode-violations-on-android-based-on-stacktrace-eb0018e909aa
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
penaltyDeath()
} else {
// Ideally, we'd use a shared thread pool but we don't have any on the system currently
// (all shared ones are coroutine dispatchers).
val executor = Executors.newSingleThreadExecutor()
penaltyListener(executor, ThreadPenaltyDeathWithIgnoresListener())
}
return this
} }

@ -0,0 +1,73 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.perf
import android.os.Build
import android.os.StrictMode
import android.os.strictmode.Violation
import androidx.annotation.RequiresApi
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.utils.ManufacturerCodes
private const val FCQN_EDM_STORAGE_PROVIDER_BASE = "com.android.server.enterprise.storage.EdmStorageProviderBase"
/**
* A [StrictMode.OnThreadViolationListener] that recreates
* [StrictMode.ThreadPolicy.Builder.penaltyDeath] but will ignore some violations. For example,
* sometimes OEMs will add code that violates StrictMode so we can ignore them here instead of
* cluttering up our code with resetAfter.
*
* This class can only be used with Android P+ so we'd have to implement workarounds if the
* violations we want to ignore affect older devices.
*/
@RequiresApi(Build.VERSION_CODES.P)
class ThreadPenaltyDeathWithIgnoresListener(
private val logger: Logger = Performance.logger
) : StrictMode.OnThreadViolationListener {
override fun onThreadViolation(violation: Violation?) {
if (violation == null) return
// Unfortunately, this method gets called many (~5+) times with the same violation so we end
// up logging/throwing redundantly.
if (shouldViolationBeIgnored(violation)) {
logger.debug("Ignoring StrictMode ThreadPolicy violation", violation)
} else {
penaltyDeath(violation)
}
}
@Suppress("TooGenericExceptionThrown") // we throw what StrictMode's penaltyDeath throws.
private fun penaltyDeath(violation: Violation) {
throw RuntimeException("StrictMode ThreadPolicy violation", violation)
}
private fun shouldViolationBeIgnored(violation: Violation): Boolean =
isSamsungLgEdmStorageProviderStartupViolation(violation)
private fun isSamsungLgEdmStorageProviderStartupViolation(violation: Violation): Boolean {
// Root issue: https://github.com/mozilla-mobile/fenix/issues/17920
//
// This fix may address the issues seen in this bug:
// https://github.com/mozilla-mobile/fenix/issues/15430
// So we might be able to back out the changes made there. However, I don't have a device to
// test so I didn't bother.
//
// This issue occurs on the Galaxy S10e, Galaxy A50, Note 10, and LG G7 FIT but not the S7:
// I'm guessing it's just a problem on recent Samsungs and LGs so it's okay being in this P+
// listener.
if (!ManufacturerCodes.isSamsung && !ManufacturerCodes.isLG) {
return false
}
// To ignore this warning, we can inspect the stack trace. There are no parts of the
// violation stack trace that are clearly unique to this violation but
// EdmStorageProviderBase doesn't appear in Android code search so we match against it.
// This class may be used in other violations that we're capable of fixing but this
// code may ignore them. I think it's okay - we keep this code simple and if it was a serious
// issue, we'd catch it on other manufacturers.
return violation.stackTrace.any { it.className == FCQN_EDM_STORAGE_PROVIDER_BASE }
}
}

@ -116,6 +116,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
} }
} }
@SuppressWarnings("LongMethod")
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -172,10 +173,13 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
val awesomeBar = view.awesome_bar val awesomeBar = view.awesome_bar
awesomeBar.customizeForBottomToolbar = requireContext().settings().shouldUseBottomToolbar awesomeBar.customizeForBottomToolbar = requireContext().settings().shouldUseBottomToolbar
val fromHomeFragment =
findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment
awesomeBarView = AwesomeBarView( awesomeBarView = AwesomeBarView(
activity, activity,
interactor, interactor,
awesomeBar awesomeBar,
fromHomeFragment
) )
view.awesome_bar.setOnTouchListener { _, _ -> view.awesome_bar.setOnTouchListener { _, _ ->
@ -191,7 +195,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
requireComponents.core.engine.speculativeCreateSession(isPrivate) requireComponents.core.engine.speculativeCreateSession(isPrivate)
if (findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment) { if (fromHomeFragment) {
// When displayed above home, dispatches the touch events to scrim area to the HomeFragment // When displayed above home, dispatches the touch events to scrim area to the HomeFragment
view.search_wrapper.background = ColorDrawable(Color.TRANSPARENT) view.search_wrapper.background = ColorDrawable(Color.TRANSPARENT)
dialog?.window?.decorView?.setOnTouchListener { _, event -> dialog?.window?.decorView?.setOnTouchListener { _, event ->
@ -467,8 +471,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
clear(pill_wrapper.id, BOTTOM) clear(pill_wrapper.id, BOTTOM)
connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP) connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP)
clear(awesome_bar.id, TOP)
clear(awesome_bar.id, BOTTOM)
connect(awesome_bar.id, TOP, search_suggestions_hint.id, BOTTOM)
connect(awesome_bar.id, BOTTOM, pill_wrapper.id, TOP)
clear(search_suggestions_hint.id, TOP) clear(search_suggestions_hint.id, TOP)
clear(search_suggestions_hint.id, BOTTOM)
connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP) connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP)
connect(search_suggestions_hint.id, BOTTOM, search_hint_bottom_barrier.id, TOP)
clear(fill_link_from_clipboard.id, TOP) clear(fill_link_from_clipboard.id, TOP)
connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP) connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP)
@ -483,7 +494,10 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) { private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) {
view?.apply { view?.apply {
val showHint = state.showSearchSuggestionsHint && !state.showSearchShortcuts val showHint = state.showSearchSuggestionsHint &&
!state.showSearchShortcuts &&
state.url != state.query
findViewById<View>(R.id.search_suggestions_hint)?.isVisible = showHint findViewById<View>(R.id.search_suggestions_hint)?.isVisible = showHint
search_suggestions_hint_divider?.isVisible = showHint search_suggestions_hint_divider?.isVisible = showHint
} }

@ -10,7 +10,6 @@ import androidx.core.graphics.BlendModeCompat.SRC_IN
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import mozilla.components.browser.awesomebar.BrowserAwesomeBar import mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.DefaultSearchEngineProvider import mozilla.components.browser.search.DefaultSearchEngineProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.concept.awesomebar.AwesomeBar import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession
@ -42,7 +41,8 @@ import mozilla.components.browser.search.SearchEngine as LegacySearchEngine
class AwesomeBarView( class AwesomeBarView(
private val activity: HomeActivity, private val activity: HomeActivity,
val interactor: AwesomeBarInteractor, val interactor: AwesomeBarInteractor,
val view: BrowserAwesomeBar val view: BrowserAwesomeBar,
private val fromHomeFragment: Boolean
) { ) {
private val sessionProvider: SessionSuggestionProvider private val sessionProvider: SessionSuggestionProvider
private val historyStorageProvider: HistoryStorageSuggestionProvider private val historyStorageProvider: HistoryStorageSuggestionProvider
@ -85,10 +85,6 @@ class AwesomeBarView(
} }
private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase { private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase {
override fun invoke(session: Session) {
interactor.onExistingSessionSelected(session.id)
}
override fun invoke(tabId: String) { override fun invoke(tabId: String) {
interactor.onExistingSessionSelected(tabId) interactor.onExistingSessionSelected(tabId)
} }
@ -111,7 +107,7 @@ class AwesomeBarView(
selectTabUseCase, selectTabUseCase,
components.core.icons, components.core.icons,
getDrawable(activity, R.drawable.ic_search_results_tab), getDrawable(activity, R.drawable.ic_search_results_tab),
excludeSelectedSession = true excludeSelectedSession = !fromHomeFragment
) )
historyStorageProvider = historyStorageProvider =

@ -31,6 +31,10 @@ class DataChoicesFragment : PreferenceFragmentCompat() {
} else { } else {
context.components.analytics.metrics.stop(MetricServiceType.Data) context.components.analytics.metrics.stop(MetricServiceType.Data)
} }
// Reset experiment identifiers on both opt-in and opt-out; it's likely
// that in future we will need to pass in the new telemetry client_id
// to this method when the user opts back in.
context.components.analytics.experiments.resetTelemetryIdentifiers()
} else if (key == getPreferenceKey(R.string.pref_key_marketing_telemetry)) { } else if (key == getPreferenceKey(R.string.pref_key_marketing_telemetry)) {
if (context.settings().isMarketingTelemetryEnabled) { if (context.settings().isMarketingTelemetryEnabled) {
context.components.analytics.metrics.start(MetricServiceType.Marketing) context.components.analytics.metrics.start(MetricServiceType.Marketing)

@ -5,9 +5,11 @@
package org.mozilla.fenix.settings package org.mozilla.fenix.settings
import android.os.Bundle import android.os.Bundle
import android.view.WindowManager
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -44,7 +46,18 @@ class PrivateBrowsingFragment : PreferenceFragmentCompat() {
} }
requirePreference<SwitchPreference>(R.string.pref_key_allow_screenshots_in_private_mode).apply { requirePreference<SwitchPreference>(R.string.pref_key_allow_screenshots_in_private_mode).apply {
onPreferenceChangeListener = SharedPreferenceUpdater() onPreferenceChangeListener = object : SharedPreferenceUpdater() {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
if ((activity as? HomeActivity)?.browsingModeManager?.mode?.isPrivate == true &&
newValue == false
) {
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
return super.onPreferenceChange(preference, newValue)
}
}
} }
} }
} }

@ -6,10 +6,7 @@ package org.mozilla.fenix.settings
import android.os.Bundle import android.os.Bundle
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
class SecretSettingsFragment : PreferenceFragmentCompat() { class SecretSettingsFragment : PreferenceFragmentCompat() {
@ -21,11 +18,5 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey) setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey)
requirePreference<SwitchPreference>(R.string.pref_key_synced_tabs_tabs_tray).apply {
isVisible = FeatureFlags.syncedTabsInTabsTray
isChecked = context.settings().syncedTabsInTabsTray
onPreferenceChangeListener = SharedPreferenceUpdater()
}
} }
} }

@ -14,6 +14,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.Toast import android.widget.Toast
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
@ -335,7 +336,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
Handler().postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
exitProcess(0) exitProcess(0)
}, AMO_COLLECTION_OVERRIDE_EXIT_DELAY) }, AMO_COLLECTION_OVERRIDE_EXIT_DELAY)
} }
@ -406,7 +407,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
getString(R.string.toast_override_fxa_sync_server_done), getString(R.string.toast_override_fxa_sync_server_done),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
Handler().postDelayed({ Handler(Looper.getMainLooper()).postDelayed({
exitProcess(0) exitProcess(0)
}, FXA_SYNC_OVERRIDE_EXIT_DELAY) }, FXA_SYNC_OVERRIDE_EXIT_DELAY)
} }
@ -434,8 +435,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
requireContext().getSystemService(RoleManager::class.java).also { requireContext().getSystemService(RoleManager::class.java).also {
if (!it.isRoleHeld(RoleManager.ROLE_BROWSER)) { if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld(
startActivityForResult(it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), 0) RoleManager.ROLE_BROWSER
)
) {
startActivityForResult(
it.createRequestRoleIntent(RoleManager.ROLE_BROWSER),
REQUEST_CODE_BROWSER_ROLE
)
} else { } else {
navigateUserToDefaultAppsSettings() navigateUserToDefaultAppsSettings()
} }
@ -452,12 +459,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
else -> { else -> {
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
(activity as HomeActivity).openToBrowserAndLoad( (activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic( searchTermOrURL = SupportUtils.getSumoURLForTopic(
requireContext(), requireContext(),
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
), ),
newTab = true, newTab = true,
from = BrowserDirection.FromSettings from = BrowserDirection.FromSettings
) )
true true
} }
@ -468,12 +475,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
/* // If the user made us the default browser, update the switch
If role manager doesn't show in-app browser changing dialog for a reason, navigate user to if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_BROWSER_ROLE) {
Default Apps Settings. updateMakeDefaultBrowserPreference()
*/
if (resultCode == Activity.RESULT_CANCELED && requestCode == 0) {
navigateUserToDefaultAppsSettings()
} }
} }
@ -547,6 +551,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
companion object { companion object {
private const val REQUEST_CODE_BROWSER_ROLE = 1
private const val SCROLL_INDICATOR_DELAY = 10L private const val SCROLL_INDICATOR_DELAY = 10L
private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L
private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L

@ -13,6 +13,7 @@ import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -111,6 +112,14 @@ class LoginDetailFragment : Fragment(R.layout.fragment_login_detail) {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
override fun onResume() {
super.onResume()
activity?.window?.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
}
/** /**
* As described in #10727, the User should re-auth if the fragment is paused and the user is not * As described in #10727, the User should re-auth if the fragment is paused and the user is not
* navigating to SavedLoginsFragment or EditLoginFragment * navigating to SavedLoginsFragment or EditLoginFragment

@ -150,6 +150,8 @@ class SavedLoginsFragment : Fragment() {
toolbarChildContainer.visibility = View.GONE toolbarChildContainer.visibility = View.GONE
(activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true) (activity as HomeActivity).getSupportActionBarAndInflateIfNecessary().setDisplayShowTitleEnabled(true)
sortingStrategyMenu.menuController.dismiss() sortingStrategyMenu.menuController.dismiss()
sortLoginsMenuRoot.setOnClickListener(null)
setHasOptionsMenu(false)
redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id) redirectToReAuth(listOf(R.id.loginDetailFragment), findNavController().currentDestination?.id)
super.onPause() super.onPause()

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

Loading…
Cancel
Save