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
* Android device: ?
* Fenix version: ?
* Device vendor / model and Android 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:
- author=mozilla-l10n-automation-bot
- status-success=pr-complete
- files~=(strings.xml)
- files~=(strings.xml|l10n.toml)
actions:
review:
type: APPROVE

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

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

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

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

@ -9,5 +9,71 @@
</head>
<body>
<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>
</html>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -9,6 +9,7 @@ import mozilla.components.browser.errorpages.ErrorType
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment
import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.Collections
@ -52,6 +53,7 @@ sealed class Event {
object CustomTabsActionTapped : Event()
object CustomTabsMenuOpened : Event()
object UriOpened : Event()
object NormalAndPrivateUriOpened : Event()
object SyncAuthOpened : Event()
object SyncAuthClosed : Event()
object SyncAuthSignUp : Event()
@ -198,12 +200,21 @@ sealed class Event {
object SyncedTabOpened : Event()
object RecentlyClosedTabsOpened : Event()
object HaveOpenTabs : Event()
object HaveNoOpenTabs : Event()
object BannerOpenInAppDisplayed : Event()
object BannerOpenInAppDismissed : Event()
object BannerOpenInAppGoToSettings : Event()
object ContextMenuCopyTapped : Event()
object ContextMenuSearchTapped : Event()
object ContextMenuSelectAllTapped : Event()
object ContextMenuShareTapped : Event()
object HaveTopSites : Event()
object HaveNoTopSites : Event()
// Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -211,6 +222,25 @@ sealed class Event {
get() = hashMapOf(TopSites.swipeCarouselKeys.page to page.toString())
}
data class SecurePrefsExperimentFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.experimentFailureKeys.failureException to failureException)
}
data class SecurePrefsGetFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.getFailureKeys.failureException to failureException)
}
data class SecurePrefsGetSuccess(val successCode: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.getResultKeys.result to successCode)
}
data class SecurePrefsWriteFailure(val failureException: String) : Event() {
override val extras =
mapOf(AndroidKeystoreExperiment.writeFailureKeys.failureException to failureException)
}
object SecurePrefsWriteSuccess : Event()
object SecurePrefsReset : Event()
data class TopSiteLongPress(val type: TopSite.Type) : Event() {
override val extras: Map<TopSites.longPressKeys, String>?
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.GleanMetrics.AboutPage
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.GleanMetrics.AndroidKeystoreExperiment
import org.mozilla.fenix.GleanMetrics.AppTheme
import org.mozilla.fenix.GleanMetrics.Autoplay
import org.mozilla.fenix.GleanMetrics.BannerOpenInApp
import org.mozilla.fenix.GleanMetrics.BookmarksManagement
import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections
@ -236,6 +238,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.UriOpened -> EventWrapper<NoExtraKeys>(
{ Events.totalUriCount.add(1) }
)
is Event.NormalAndPrivateUriOpened -> EventWrapper<NoExtraKeys>(
{ Events.normalAndPrivateUriCount.add(1) }
)
is Event.ErrorPageVisited -> EventWrapper(
{ ErrorPage.visitedError.record(it) },
{ ErrorPage.visitedErrorKeys.valueOf(it) }
@ -723,6 +728,51 @@ private val Event.wrapper: EventWrapper<*>?
is Event.ContextMenuShareTapped -> EventWrapper<NoExtraKeys>(
{ 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:
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.prompts.dialog.LoginDialogFacts
import mozilla.components.feature.pwa.ProgressiveWebAppFacts
import mozilla.components.feature.top.sites.facts.TopSitesFacts
import mozilla.components.lib.dataprotect.SecurePrefsReliabilityExperiment
import mozilla.components.support.base.Component
import mozilla.components.support.base.facts.Action
import mozilla.components.support.base.facts.Fact
@ -75,6 +77,7 @@ internal class DebugMetricController(
}
@VisibleForTesting
@Suppress("LargeClass")
internal class ReleaseMetricController(
private val services: List<MetricsService>,
private val isDataTelemetryEnabled: () -> Boolean,
@ -134,6 +137,13 @@ internal class ReleaseMetricController(
}
}
@VisibleForTesting
internal fun factToEvent(
fact: Fact
): Event? {
return fact.toEvent()
}
private fun isInitialized(type: MetricServiceType): Boolean = initialized.contains(type)
private fun isTelemetryEnabled(type: MetricServiceType): Boolean = when (type) {
@ -242,6 +252,43 @@ internal class ReleaseMetricController(
Component.FEATURE_PWA to ProgressiveWebAppFacts.Items.INSTALL_SHORTCUT -> {
Event.ProgressiveWebAppInstallAsShortcut
}
Component.FEATURE_TOP_SITES to TopSitesFacts.Items.COUNT -> {
value?.let {
var count = 0
try {
count = it.toInt()
} catch (e: NumberFormatException) {
// Do nothing
}
return if (count > 0) {
Event.HaveTopSites
} else {
Event.HaveNoTopSites
}
}
null
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.EXPERIMENT -> {
Event.SecurePrefsExperimentFailure(metadata?.get("javaClass") as String? ?: "null")
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.GET -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsGetFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsGetSuccess(value ?: "")
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.WRITE -> {
if (SecurePrefsReliabilityExperiment.Companion.Values.FAIL.v == value?.toInt()) {
Event.SecurePrefsWriteFailure(metadata?.get("javaClass") as String? ?: "null")
} else {
Event.SecurePrefsWriteSuccess
}
}
Component.LIB_DATAPROTECT to SecurePrefsReliabilityExperiment.Companion.Actions.RESET -> {
Event.SecurePrefsReset
}
else -> null
}

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

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

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

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

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

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

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

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

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

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

@ -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].
*/
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) })
}

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

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

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

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

@ -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) :
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 isFirstRun = true
fun updateData(tree: BookmarkNode?, mode: BookmarkFragmentState.Mode) {
// Display folders above all other bookmarks.
val allNodes = tree?.children.orEmpty()
val folders: MutableList<BookmarkNode> = mutableListOf()
val notFolders: MutableList<BookmarkNode> = mutableListOf()
val separators: MutableList<BookmarkNode> = mutableListOf()
allNodes.forEach {
if (it.type == BookmarkNodeType.FOLDER) {
folders.add(it)
} else {
notFolders.add(it)
when (it.type) {
BookmarkNodeType.SEPARATOR -> separators.add(it)
BookmarkNodeType.FOLDER -> folders.add(it)
else -> notFolders.add(it)
}
}
val newTree = folders + notFolders
// Display folders above all other bookmarks. Exclude separators.
// For separator removal, see discussion in https://github.com/mozilla-mobile/fenix/issues/15214
val newTree = folders + notFolders - separators
val diffUtil = DiffUtil.calculateDiff(
BookmarkDiffUtil(

@ -25,6 +25,7 @@ import mozilla.components.browser.state.state.BrowserState
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.content.DownloadState
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.HomeActivity
@ -50,6 +51,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
private lateinit var metrics: MetricController
private var undoScope: CoroutineScope? = null
private var pendingDownloadDeletionJob: (suspend () -> Unit)? = null
private lateinit var downloadsUseCases: DownloadsUseCases
override fun onCreateView(
inflater: LayoutInflater,
@ -59,6 +61,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
val view = inflater.inflate(R.layout.fragment_downloads, container, false)
val items = provideDownloads(requireComponents.core.store.state)
downloadsUseCases = requireContext().components.useCases.downloadUseCases
downloadStore = StoreProvider.get(this) {
DownloadFragmentStore(
@ -85,6 +88,10 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
return view
}
/**
* Returns a list of available downloads to be displayed to the user.
* Downloads must be COMPLETED and existent on disk.
*/
@VisibleForTesting
internal fun provideDownloads(state: BrowserState): List<DownloadItem> {
return state.downloads.values
@ -128,9 +135,7 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
setPositiveButton(R.string.delete_browsing_data_prompt_allow) { dialog: DialogInterface, _ ->
// Use fragment's lifecycle; the view may be gone by the time dialog is interacted with.
lifecycleScope.launch(IO) {
context.let {
it.components.useCases.downloadUseCases.removeAllDownloads()
}
downloadsUseCases.removeAllDownloads()
updatePendingDownloadToDelete(downloadStore.state.items.toSet())
launch(Dispatchers.Main) {
showSnackBar(
@ -146,6 +151,11 @@ class DownloadFragment : LibraryPageFragment<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>) {
metrics.track(Event.DownloadsItemDeleted)
@ -155,10 +165,10 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
requireView(),
getMultiSelectSnackBarMessage(items),
getString(R.string.bookmark_undo_deletion),
{
onCancel = {
undoPendingDeletion(items)
},
getDeleteDownloadItemsOperation(items)
operation = getDeleteDownloadItemsOperation(downloadsUseCases, items)
)
}
@ -210,6 +220,9 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
else -> super.onOptionsItemSelected(item)
}
/**
* Provides a message to the Undo snackbar.
*/
private fun getMultiSelectSnackBarMessage(downloadItems: Set<DownloadItem>): String {
return if (downloadItems.size > 1) {
getString(R.string.download_delete_multiple_items_snackbar_1)
@ -246,14 +259,18 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
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 {
CoroutineScope(IO).launch {
downloadStore.dispatch(DownloadFragmentAction.EnterDeletionMode)
context?.let {
for (item in items) {
it.components.useCases.downloadUseCases.removeDownload(item.id)
}
for (item in items) {
downloadUseCases.removeDownload(item.id)
}
downloadStore.dispatch(DownloadFragmentAction.ExitDeletionMode)
pendingDownloadDeletionJob = null
@ -261,8 +278,14 @@ class DownloadFragment : LibraryPageFragment<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>) {
pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(items)
pendingDownloadDeletionJob = getDeleteDownloadItemsOperation(downloadsUseCases, items)
val ids = items.map { item -> item.id }.toSet()
downloadStore.dispatch(DownloadFragmentAction.AddPendingDeletionSet(ids))
}
@ -273,6 +296,9 @@ class DownloadFragment : LibraryPageFragment<DownloadItem>(), UserInteractionHan
downloadStore.dispatch(DownloadFragmentAction.UndoPendingDeletionSet(ids))
}
/**
* Executes pending job(s) when leaving [DownloadFragment].
*/
private fun invokePendingDeletion() {
pendingDownloadDeletionJob?.let {
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) {
telemetry.frameworkStartError.set(true)
} else {
val clockTicksPerSecond = stat.clockTicksPerSecond.also {
// framework* is derived from the number of clock ticks per second. To ensure this
// value does not throw off our result, we capture it too.
telemetry.clockTicksPerSecond.add(it.toInt())
}
// In our brief analysis, clock ticks per second was overwhelmingly equal to 100. To make
// analysis easier in GLAM, we split the results into two separate metrics. See the
// metric descriptions for more details.
@Suppress("MagicNumber") // it's more confusing to separate the comment above from the value declaration.
val durationMetric =
if (clockTicksPerSecond == 100L) telemetry.frameworkPrimary else telemetry.frameworkSecondary
try {
telemetry.frameworkStart.setRawNanos(getFrameworkStartNanos())
durationMetric.setRawNanos(getFrameworkStartNanos())
} catch (e: FileNotFoundException) {
// Privacy managers can add hooks that block access to reading system /proc files.
// We want to catch these exception and report an error on accessing the file
// rather than an implementation error.
telemetry.frameworkStartReadError.set(true)
}
// frameworkStart is derived from the number of clock ticks per second. To ensure this
// value does not throw off our result, we capture it too.
telemetry.clockTicksPerSecond.add(stat.clockTicksPerSecond.toInt())
}
}
/**
* @throws [java.io.FileNotFoundException]
*/

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

@ -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(
inflater: LayoutInflater,
container: ViewGroup?,
@ -172,10 +173,13 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
val awesomeBar = view.awesome_bar
awesomeBar.customizeForBottomToolbar = requireContext().settings().shouldUseBottomToolbar
val fromHomeFragment =
findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment
awesomeBarView = AwesomeBarView(
activity,
interactor,
awesomeBar
awesomeBar,
fromHomeFragment
)
view.awesome_bar.setOnTouchListener { _, _ ->
@ -191,7 +195,7 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
requireComponents.core.engine.speculativeCreateSession(isPrivate)
if (findNavController().previousBackStackEntry?.destination?.id == R.id.homeFragment) {
if (fromHomeFragment) {
// When displayed above home, dispatches the touch events to scrim area to the HomeFragment
view.search_wrapper.background = ColorDrawable(Color.TRANSPARENT)
dialog?.window?.decorView?.setOnTouchListener { _, event ->
@ -467,8 +471,15 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
clear(pill_wrapper.id, BOTTOM)
connect(pill_wrapper.id, BOTTOM, toolbar.id, TOP)
clear(awesome_bar.id, TOP)
clear(awesome_bar.id, BOTTOM)
connect(awesome_bar.id, TOP, search_suggestions_hint.id, BOTTOM)
connect(awesome_bar.id, BOTTOM, pill_wrapper.id, TOP)
clear(search_suggestions_hint.id, TOP)
clear(search_suggestions_hint.id, BOTTOM)
connect(search_suggestions_hint.id, TOP, PARENT_ID, TOP)
connect(search_suggestions_hint.id, BOTTOM, search_hint_bottom_barrier.id, TOP)
clear(fill_link_from_clipboard.id, TOP)
connect(fill_link_from_clipboard.id, BOTTOM, pill_wrapper.id, TOP)
@ -483,7 +494,10 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
private fun updateSearchSuggestionsHintVisibility(state: SearchFragmentState) {
view?.apply {
val showHint = state.showSearchSuggestionsHint && !state.showSearchShortcuts
val showHint = state.showSearchSuggestionsHint &&
!state.showSearchShortcuts &&
state.url != state.query
findViewById<View>(R.id.search_suggestions_hint)?.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 mozilla.components.browser.awesomebar.BrowserAwesomeBar
import mozilla.components.browser.search.DefaultSearchEngineProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.concept.awesomebar.AwesomeBar
import mozilla.components.concept.engine.EngineSession
@ -42,7 +41,8 @@ import mozilla.components.browser.search.SearchEngine as LegacySearchEngine
class AwesomeBarView(
private val activity: HomeActivity,
val interactor: AwesomeBarInteractor,
val view: BrowserAwesomeBar
val view: BrowserAwesomeBar,
private val fromHomeFragment: Boolean
) {
private val sessionProvider: SessionSuggestionProvider
private val historyStorageProvider: HistoryStorageSuggestionProvider
@ -85,10 +85,6 @@ class AwesomeBarView(
}
private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase {
override fun invoke(session: Session) {
interactor.onExistingSessionSelected(session.id)
}
override fun invoke(tabId: String) {
interactor.onExistingSessionSelected(tabId)
}
@ -111,7 +107,7 @@ class AwesomeBarView(
selectTabUseCase,
components.core.icons,
getDrawable(activity, R.drawable.ic_search_results_tab),
excludeSelectedSession = true
excludeSelectedSession = !fromHomeFragment
)
historyStorageProvider =

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

@ -5,9 +5,11 @@
package org.mozilla.fenix.settings
import android.os.Bundle
import android.view.WindowManager
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.PrivateShortcutCreateManager
import org.mozilla.fenix.components.metrics.Event
@ -44,7 +46,18 @@ class PrivateBrowsingFragment : PreferenceFragmentCompat() {
}
requirePreference<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 androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar
class SecretSettingsFragment : PreferenceFragmentCompat() {
@ -21,11 +18,5 @@ class SecretSettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.secret_settings_preferences, rootKey)
requirePreference<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.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.widget.Toast
import androidx.annotation.VisibleForTesting
@ -335,7 +336,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
Toast.LENGTH_LONG
).show()
Handler().postDelayed({
Handler(Looper.getMainLooper()).postDelayed({
exitProcess(0)
}, AMO_COLLECTION_OVERRIDE_EXIT_DELAY)
}
@ -406,7 +407,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
getString(R.string.toast_override_fxa_sync_server_done),
Toast.LENGTH_LONG
).show()
Handler().postDelayed({
Handler(Looper.getMainLooper()).postDelayed({
exitProcess(0)
}, FXA_SYNC_OVERRIDE_EXIT_DELAY)
}
@ -434,8 +435,14 @@ class SettingsFragment : PreferenceFragmentCompat() {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
Preference.OnPreferenceClickListener {
requireContext().getSystemService(RoleManager::class.java).also {
if (!it.isRoleHeld(RoleManager.ROLE_BROWSER)) {
startActivityForResult(it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), 0)
if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld(
RoleManager.ROLE_BROWSER
)
) {
startActivityForResult(
it.createRequestRoleIntent(RoleManager.ROLE_BROWSER),
REQUEST_CODE_BROWSER_ROLE
)
} else {
navigateUserToDefaultAppsSettings()
}
@ -452,12 +459,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
else -> {
Preference.OnPreferenceClickListener {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
),
newTab = true,
from = BrowserDirection.FromSettings
searchTermOrURL = SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
),
newTab = true,
from = BrowserDirection.FromSettings
)
true
}
@ -468,12 +475,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
/*
If role manager doesn't show in-app browser changing dialog for a reason, navigate user to
Default Apps Settings.
*/
if (resultCode == Activity.RESULT_CANCELED && requestCode == 0) {
navigateUserToDefaultAppsSettings()
// If the user made us the default browser, update the switch
if (resultCode == Activity.RESULT_OK && requestCode == REQUEST_CODE_BROWSER_ROLE) {
updateMakeDefaultBrowserPreference()
}
}
@ -547,6 +551,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
companion object {
private const val REQUEST_CODE_BROWSER_ROLE = 1
private const val SCROLL_INDICATOR_DELAY = 10L
private const val FXA_SYNC_OVERRIDE_EXIT_DELAY = 2000L
private const val AMO_COLLECTION_OVERRIDE_EXIT_DELAY = 3000L

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

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

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

Loading…
Cancel
Save