Merge tag 'v89.1.1' into upstream-sync

pull/420/head
Adam Novak 3 years ago
commit c667cfa0ae

@ -0,0 +1,43 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/
name: "Sync Strings"
on:
schedule:
- cron: '0 */4 * * *'
jobs:
main:
name: "Sync Strings"
runs-on: ubuntu-20.04
steps:
- name: "Discover Fenix Beta Version"
id: fenix-beta-version
uses: mozilla-mobile/fenix-beta-version@1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: "Checkout Master Branch"
uses: actions/checkout@v2
with:
path: main
ref: master
- name: "Checkout Beta Branch"
uses: actions/checkout@v2
with:
path: beta
ref: "releases_v${{ steps.fenix-beta-version.outputs.fenix-beta-version }}.0.0"
- name: "Sync Strings"
uses: mozilla-mobile/sync-strings-action@1.0.1
with:
src: main
dst: beta
- name: Create Pull Request
uses: peter-evans/create-pull-request@v3
with:
token: ${{ secrets.GITHUB_TOKEN }}
path: beta
branch: automation/sync-strings-${{ steps.fenix-beta-version.outputs.major-beta-version }}
title: "Sync Strings from master to releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0"
body: "This (automated) PR syncs strings from `master` to `releases_${{steps.fenix-beta-version.outputs.fenix-beta-version}}.0.0`"

1
.gitignore vendored

@ -80,7 +80,6 @@ gen-external-apklibs
.DS_Store .DS_Store
# Secrets files, e.g. tokens # Secrets files, e.g. tokens
.leanplum_token
.adjust_token .adjust_token
.sentry_token .sentry_token
.mls_token .mls_token

@ -255,7 +255,7 @@ android {
// instead. :) // instead. :)
maxParallelForks = 2 maxParallelForks = 2
forkEvery = 80 forkEvery = 80
maxHeapSize = "2048m" maxHeapSize = "3072m"
minHeapSize = "1024m" minHeapSize = "1024m"
} }
} }
@ -362,25 +362,6 @@ android.applicationVariants.all { variant ->
println("--") println("--")
} }
// -------------------------------------------------------------------------------------------------
// Leanplum: Read token from local file if it exists
// -------------------------------------------------------------------------------------------------
print("Leanplum token: ")
try {
def parts = new File("${rootDir}/.leanplum_token").text.trim().split(":")
def id = parts[0]
def key = parts[1]
buildConfigField 'String', 'LEANPLUM_ID', '"' + id + '"'
buildConfigField 'String', 'LEANPLUM_TOKEN', '"' + key + '"'
println "(Added from .leanplum_token file)"
} catch (FileNotFoundException ignored) {
buildConfigField 'String', 'LEANPLUM_ID', 'null'
buildConfigField 'String', 'LEANPLUM_TOKEN', 'null'
println("X_X")
}
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
// MLS: Read token from local file if it exists // MLS: Read token from local file if it exists
// ------------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------------
@ -483,7 +464,6 @@ dependencies {
implementation Deps.sentry implementation Deps.sentry
implementation Deps.mozilla_concept_base implementation Deps.mozilla_concept_base
implementation Deps.mozilla_concept_engine implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu implementation Deps.mozilla_concept_menu

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
--- ---
$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0 $schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
no_lint: no_lint:
- CATEGORY_GENERIC - CATEGORY_GENERIC
@ -11,6 +11,12 @@ events:
app_opened_all_startup: app_opened_all_startup:
type: event type: event
description: | description: |
**This probe has a known flaw:** for COLD start up, it doesn't take into
account if the process is already running when the app starts, possibly
inflating results (e.g. a Service started the process 20min ago and only
now is HomeActivity launching). See the `cold_*_app_to_first_frame` probes
for a replacement.
<br><br>
A user opened the app to the HomeActivity. The HomeActivity A user opened the app to the HomeActivity. The HomeActivity
encompasses the home screen, browser screen, settings screen, encompasses the home screen, browser screen, settings screen,
collections and other screens in the nav_graph. collections and other screens in the nav_graph.
@ -217,6 +223,32 @@ events:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-07-01" expires: "2021-07-01"
default_browser_changed:
type: event
description: |
Indicates the default browser was changed
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18857
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18895
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
toolbar_menu_visible:
type: event
description: |
The browser menu was displayed from toolbar menu
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18855
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18895
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
total_uri_count: total_uri_count:
type: counter type: counter
description: | description: |
@ -555,6 +587,20 @@ search_shortcuts:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
experiments_default_browser:
toolbar_menu_clicked:
type: event
description: |
Set default browser was clicked from toolbar menu
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18851
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18895
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
toolbar_settings: toolbar_settings:
changed_position: changed_position:
type: event type: event
@ -1187,6 +1233,40 @@ metrics:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
start_reason_process_error:
type: boolean
description: |
The `AppStartReasonProvider.ProcessLifecycleObserver.onCreate` was
unexpectedly called twice. We can use this metric to validate our
assumptions about how these APIs are called. This probe can be removed
once we validate these assumptions.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18426
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
start_reason_activity_error:
type: boolean
description: |
The `AppStartReasonProvider.ActivityLifecycleCallbacks.onActivityCreated`
was unexpectedly called twice. We can use this metric to validate our
assumptions about how these APIs are called. This probe can be removed
once we validate these assumptions.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18426
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
preferences: preferences:
show_search_suggestions: show_search_suggestions:
@ -2433,6 +2513,19 @@ tabs_tray:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
synced_mode_tapped:
type: event
description: |
A user switched to synced mode
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18948
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19004
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2022-08-01"
new_tab_tapped: new_tab_tapped:
type: event type: event
description: | description: |
@ -3938,6 +4031,23 @@ addons:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-07-01" expires: "2021-07-01"
open_addon_setting:
type: event
description: |
A user opened an add-on's setting
extra_keys:
addon_id:
description: |
The id of the add-on that was interacted with
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17644
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18504
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2022-08-01"
has_installed_addons: has_installed_addons:
type: boolean type: boolean
description: | description: |
@ -4126,6 +4236,95 @@ startup.timeline:
expires: "2021-08-01" expires: "2021-08-01"
perf.startup: perf.startup:
cold_main_app_to_first_frame:
type: timing_distribution
time_unit: millisecond
description: |
The duration from `*Application`'s initializer to the first Android frame
being drawn in a [COLD MAIN start
up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary).
Notably, this duration omits the time from process start to the
initializer (which includes a lengthy dex operation) and the time from
the first frame to visual completeness. This probe doesn't measure Custom
Tabs or other uses of `ExternalAppBrowserActivity` to simplify result
analysis. The methodology for determining this measurement is imperfect
to simplify implementation. Issues may include:
<br>- Not measuring Beta and Release channels (due to
`MigrationDecisionActivity` interrupting the logic).
<br>- Not distinguishing between MAIN to homescreen, onboarding, session
restore, others?
<br>- Not choosing to record a MAIN based on what the user would see and
thus the core code path (i.e. the thing we want to measure) but rather on
the initial `Intent` state.
<br><br>
The hope is that these cases will not have a significant impact on the end
results but, if they appear to, we can replace it with a more complex
implementation.
<br><br>
Around April 8, 2021 the implementation was refactored. Functionally, it
should be the same but it's noted just in case there are bugs.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18426
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
cold_view_app_to_first_frame:
type: timing_distribution
time_unit: millisecond
description: |
The duration from `*Application`'s initializer to the first Android frame
being drawn in a [COLD VIEW start
up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary).
The methodology for determining this measurement is imperfect to simplify
implementation. Issues may include:
<br>-Including VIEW intents that aren't valid so take code paths similar
to MAIN (this is speculative)
<br><br>
See the `cold_main_app_to_first_frame` probe docs for other possible
known issues and more details.
<br><br>
Around April 8, 2021 the implementation was refactored. Functionally, it
should be the same but it's noted just in case there are bugs.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18426
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
cold_unknwn_app_to_first_frame:
type: timing_distribution
time_unit: millisecond
description: |
The duration from `*Application`'s initializer to the first Android frame
being drawn in a [COLD start
up](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary)
where we can't say it was a MAIN or VIEW start up. The methodology for
determining this measurement is imperfect to simplify implementation.
<br><br>
See the `cold_main_app_to_first_frame` probe docs for known issues and
more details.
<br><br>
Around April 8, 2021 the implementation was refactored. Functionally, it
should be the same but it's noted just in case there are bugs.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18426
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18632#issue-600193452
data_sensitivity:
- technical
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-08-11"
application_on_create: application_on_create:
type: timing_distribution type: timing_distribution
time_unit: millisecond time_unit: millisecond
@ -4293,6 +4492,81 @@ perf.startup:
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-08-11" expires: "2021-08-11"
startup_type:
type: labeled_counter
description: |
Indicates how the browser was started. The label is divided into two
variables. `state` is how cached the browser is when started. `path` is
what code path we are expected to take. Together, they create a combined
label: `state_path`. For brevity, the specific states are documented in
the [Fenix perf
glossary](https://wiki.mozilla.org/index.php?title=Performance/Fenix/Glossary).
<br><br>
This implementation is intended to be simple, not comprehensive. We list
the implications below.
<br><br>
These ways of opening the app undesirably adds events to our primary
buckets (non-`unknown` cases):
<br>- App switcher cold/warm: `cold/warm_` + duplicates path from
previous launch
<br>- Home screen shortcuts: `*_view`
<br>- An Intent is sent internally that's uses `ACTION_MAIN` or
`ACTION_VIEW` could be: `*_main/view` (unknown if this ever happens)
<br>- A command-line launch uses `ACTION_MAIN` or `ACTION_VIEW` could be:
`*_main/view`
<br><br>
These ways of opening the app undesirably do not add their events to our
primary buckets:
<br>- Close and reopen the app very quickly: no event is recorded.
<br><br>
These ways of opening the app don't affect our primary buckets:
<br>- App switcher hot: `hot_unknown`
<br>- PWA (all states): `unknown_unknown`
<br>- Custom tab: `unknown_view`
<br>- Cold start where a service or other non-activity starts the process
(not manually tested) - this seems to happen if you have the homescreen
widget: `unknown_*`
<br>- Another activity is drawn before HomeActivity (e.g. widget voice
search): `unknown_*`
<br>- Widget text search: `*_unknown`
<br><br>
In addition to the events above, the `unknown` state may be chosen when we
were unable to determine a cause due to implementation details or the API
was used incorrectly. We may be able to record the events listed above
into different buckets but we kept the implementation simple for now.
<br><br>
N.B.: for implementation simplicity, we duplicate the logic in app that
determines `path` so it's not perfectly accurate. In one way, we record we
is intended to happen rather than what actually happened (e.g. the user
may click a link so we record VIEW but the app does a MAIN by going to the
homescreen because the link was invalid).
labels:
- cold_main
- cold_view
- cold_unknown
- warm_main
- warm_view
- warm_unknown
- hot_main
- hot_view
- hot_unknown
- unknown_main
- unknown_view
- unknown_unknown
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18836
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19028
data_sensitivity:
- interaction
notification_emails:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-10-09"
perf.awesomebar: perf.awesomebar:
history_suggestions: history_suggestions:
@ -4312,7 +4586,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
bookmark_suggestions: bookmark_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4330,7 +4604,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
search_engine_suggestions: search_engine_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4348,7 +4622,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
session_suggestions: session_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4366,7 +4640,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
synced_tabs_suggestions: synced_tabs_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4384,7 +4658,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
clipboard_suggestions: clipboard_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4402,7 +4676,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
shortcuts_suggestions: shortcuts_suggestions:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4420,7 +4694,7 @@ perf.awesomebar:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- gkruglov@mozilla.com - gkruglov@mozilla.com
expires: "2020-11-15" expires: "2021-11-15"
autoplay: autoplay:
visited_setting: visited_setting:
@ -4811,6 +5085,46 @@ engine_tab:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- skaspari@mozilla.com - skaspari@mozilla.com
expires: "2021-12-31" expires: "2021-12-31"
foreground_metrics:
type: event
description: |
Event collecting data about the state of tabs when the app comes back to
the foreground.
bugs:
- https://github.com/mozilla-mobile/android-components/issues/9997
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18747#issuecomment-815731764
data_sensitivity:
- technical
notification_emails:
- fenix-core@mozilla.com
- skaspari@mozilla.com
expires: "2021-12-31"
extra_keys:
background_active_tabs:
description: |
Number of active tabs (with an engine session assigned) when the app
went to the background.
background_crashed_tabs:
description: |
Number of tabs marked as crashed when the app went to the background.
background_total_tabs:
description: |
Number of total tabs when the app went to the background.
foreground_active_tabs:
description: |
Number of active tabs (with an engine session assigned) when the
app came back to the foreground.
foreground_crashed_tabs:
description: |
Number of tabs marked as crashed when the app came back to the
foreground.
foreground_total_tabs:
description: |
Number of total tabs when the app came back to the foreground.
time_in_background:
description: |
Time (in milliseconds) the app was in the background.
synced_tabs: synced_tabs:
synced_tabs_suggestion_clicked: synced_tabs_suggestion_clicked:
@ -4907,6 +5221,34 @@ awesomebar:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
home_menu:
settings_item_clicked:
type: event
description: The user clicked the settings option in home menu.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18856
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18987
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
home_screen:
home_screen_displayed:
type: event
description: The user clicked the settings option in home menu.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18856
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19025
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
android_keystore_experiment: android_keystore_experiment:
experiment_failure: experiment_failure:
type: event type: event
@ -5007,3 +5349,46 @@ android_keystore_experiment:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-09-01" expires: "2021-09-01"
set_default_newtab_experiment:
set_default_browser_clicked:
type: event
description: |
Set default browser was clicked from new tab screen.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18853
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18895
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
close_experiment_card_clicked:
type: event
description: |
Close experiment card was clicked from new tab screen.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18853
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/18895
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"
set_default_setting_experiment:
set_default_browser_clicked:
type: event
description: |
Set default browser was clicked from settings screen.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/18852
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/19047
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-10-01"

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this # License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/. # file, You can obtain one at http://mozilla.org/MPL/2.0/.
--- ---
$schema: moz://mozilla.org/schemas/glean/pings/1-0-0 $schema: moz://mozilla.org/schemas/glean/pings/2-0-0
activation: activation:
description: | description: |

@ -21,7 +21,7 @@ import org.mozilla.fenix.helpers.HomeActivityTestRule
// BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM. // BEFORE INCREASING THESE VALUES, PLEASE CONSULT WITH THE PERF TEAM.
private const val EXPECTED_SUPPRESSION_COUNT = 11 private const val EXPECTED_SUPPRESSION_COUNT = 11
private const val EXPECTED_RUNBLOCKING_COUNT = 2 private const val EXPECTED_RUNBLOCKING_COUNT = 3
private const val EXPECTED_COMPONENT_INIT_COUNT = 42 private const val EXPECTED_COMPONENT_INIT_COUNT = 42
private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12 private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4 private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4

@ -31,8 +31,8 @@ class DefaultHomeScreenTest : ScreenshotTest() {
@Test @Test
fun showDefaultHomeScreen() { fun showDefaultHomeScreen() {
homeScreen { homeScreen {
verifyAccountsSignInButton()
swipeToBottom() swipeToBottom()
verifyAccountsSignInButton()
Screengrab.screenshot("HomeScreenRobot_home-screen-scroll") Screengrab.screenshot("HomeScreenRobot_home-screen-scroll")
TestAssetHelper.waitingTime TestAssetHelper.waitingTime
} }

@ -142,8 +142,8 @@ class MenuScreenShotTest : ScreenshotTest() {
editBookmarkFolder() editBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu") Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu")
// It may be needed to wait here to have the screenshot // It may be needed to wait here to have the screenshot
mDevice.pressBack()
bookmarksMenu { bookmarksMenu {
navigateUp()
}.openThreeDotMenu("test") { }.openThreeDotMenu("test") {
deleteBookmarkFolder() deleteBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_delete-bookmark-folder-menu") Screengrab.screenshot("ThreeDotMenuBookmarksRobot_delete-bookmark-folder-menu")

@ -28,7 +28,6 @@ import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.ext.toUri import org.mozilla.fenix.helpers.ext.toUri
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.accountSettings import org.mozilla.fenix.ui.robots.accountSettings
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword
@ -141,10 +140,7 @@ class SyncIntegrationTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage) { }.enterURLAndEnterToBrowser(defaultWebPage) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddBookmarkButton() }.bookmarkPage {
clickAddBookmarkButton()
}
browserScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openSettings { }.openSettings {
}.openTurnOnSyncMenu { }.openTurnOnSyncMenu {

@ -21,7 +21,7 @@ class GradlewBuild(object):
# Change path accordingly to go to root folder to run gradlew # Change path accordingly to go to root folder to run gradlew
os.chdir('../../../../../../../..') os.chdir('../../../../../../../..')
cmd = './gradlew ' + 'app:connectedGeckoNightlyDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}'.format(identifier) cmd = './gradlew ' + 'app:connectedDebugAndroidTest -Pandroid.testInstrumentationRunnerArguments.class=org.mozilla.fenix.syncintegration.SyncIntegrationTest#{}'.format(identifier)
self.logger.info('Running cmd: {}'.format(cmd)) self.logger.info('Running cmd: {}'.format(cmd))

@ -12,7 +12,6 @@ import mozilla.appservices.places.BookmarkRoot
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -32,7 +31,6 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
/** /**
* Tests for verifying basic functionality of bookmarks * Tests for verifying basic functionality of bookmarks
*/ */
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
class BookmarksTest { class BookmarksTest {
/* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping. /* ktlint-disable no-blank-line-before-rbrace */ // This imposes unreadable grouping.
@ -96,10 +94,7 @@ class BookmarksTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddBookmarkButton() }.bookmarkPage {
clickAddBookmarkButton()
}
browserScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyEditBookmarkButton() verifyEditBookmarkButton()
} }

@ -32,6 +32,7 @@ import org.mozilla.fenix.ui.robots.navigationToolbar
* *
*/ */
@Ignore("Test failures: https://github.com/mozilla-mobile/fenix/issues/18421")
class ContextMenusTest { class ContextMenusTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer

@ -88,31 +88,11 @@ class NavigationToolbarTest {
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMenuExists() verifyThreeDotMenuExists()
verifyForwardButton()
}.goForward { }.goForward {
verifyUrl(nextWebPage.url.toString()) verifyUrl(nextWebPage.url.toString())
} }
} }
@Test
fun refreshPageTest() {
val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(refreshWebPage.url) {
mDevice.waitForIdle()
}
// Use refresh from the three-dot menu
navigationToolbar {
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyRefreshButton()
}.refreshPage {
verifyPageContent("REFRESHED")
}
}
@Test @Test
fun visitURLTest() { fun visitURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

@ -78,7 +78,7 @@ class NoNetworkAccessStartupTests {
browserScreen { browserScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.refreshPage {} }.refreshPage { }
} }
@Test @Test

@ -9,10 +9,8 @@ import androidx.test.espresso.IdlingRegistry
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityIntentTestRule import org.mozilla.fenix.helpers.HomeActivityIntentTestRule
@ -104,7 +102,6 @@ class ReaderViewTest {
@Test @Test
fun verifyReaderViewToggle() { fun verifyReaderViewToggle() {
// New three-dot menu design does not have readerview appearance menu item
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -127,34 +124,21 @@ class ReaderViewTest {
} }
browserScreen { browserScreen {
verifyPageContent(estimatedReadingTime) verifyPageContent(estimatedReadingTime)
} }.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.closeBrowserMenuToBrowser { }
navigationToolbar { navigationToolbar {
verifyCloseReaderViewDetected(true) verifyCloseReaderViewDetected(true)
toggleReaderView() toggleReaderView()
mDevice.waitForIdle() mDevice.waitForIdle()
verifyReaderViewDetected(true) verifyReaderViewDetected(true)
} }.openThreeDotMenu {
verifyReaderViewAppearance(false)
if (!FeatureFlags.toolbarMenuFeature) { }.close { }
browserScreen {
verifyPageContent(estimatedReadingTime)
}.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.closeBrowserMenuToBrowser { }
}
if (!FeatureFlags.toolbarMenuFeature) {
navigationToolbar {
toggleReaderView()
mDevice.waitForIdle()
}.openThreeDotMenu {
verifyReaderViewAppearance(false)
}.close { }
}
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceFontToggle() { fun verifyReaderViewAppearanceFontToggle() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -195,7 +179,6 @@ class ReaderViewTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceFontSizeToggle() { fun verifyReaderViewAppearanceFontSizeToggle() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)
@ -242,7 +225,6 @@ class ReaderViewTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
fun verifyReaderViewAppearanceColorSchemeChange() { fun verifyReaderViewAppearanceColorSchemeChange() {
val readerViewPage = val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer) TestAssetHelper.getLoremIpsumAsset(mockWebServer)

@ -106,7 +106,6 @@ class SettingsBasicsTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
fun toggleShowVisitedSitesAndBookmarks() { fun toggleShowVisitedSitesAndBookmarks() {
// Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions. // Bookmarks a few websites, toggles the history and bookmarks setting to off, then verifies if the visited and bookmarked websites do not show in the suggestions.
val page1 = getGenericAsset(mockWebServer, 1) val page1 = getGenericAsset(mockWebServer, 1)
@ -117,15 +116,13 @@ class SettingsBasicsTest {
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(page1.url) { }.enterURLAndEnterToBrowser(page1.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
clickAddBookmarkButton() }.bookmarkPage { }
}
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(page2.url) { }.enterURLAndEnterToBrowser(page2.url) {
verifyUrl(page2.url.toString()) verifyUrl(page2.url.toString())
}.openThreeDotMenu { }.openThreeDotMenu {
clickAddBookmarkButton() }.bookmarkPage { }
}
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(page3.url) { }.enterURLAndEnterToBrowser(page3.url) {
@ -137,6 +134,7 @@ class SettingsBasicsTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/19016")
@Test @Test
fun changeThemeSetting() { fun changeThemeSetting() {
// Goes through the settings and changes the default search engine, then verifies it changes. // Goes through the settings and changes the default search engine, then verifies it changes.
@ -163,6 +161,7 @@ class SettingsBasicsTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/18986")
@Test @Test
fun changeAccessibiltySettings() { fun changeAccessibiltySettings() {
// Goes through the settings and changes the default text on a webpage, then verifies if the text has changed. // Goes through the settings and changes the default text on a webpage, then verifies if the text has changed.

@ -319,6 +319,7 @@ class SettingsPrivacyTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen { }.openAddToHomeScreen {
addShortcutName(pageShortcutName) addShortcutName(pageShortcutName)
clickAddShortcutButton() clickAddShortcutButton()

@ -56,7 +56,6 @@ class ShareButtonTest {
// From the 3-dot menu next to the Select share menu // From the 3-dot menu next to the Select share menu
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyShareButton()
clickShareButton() clickShareButton()
verifyShareScrim() verifyShareScrim()
verifySendToDeviceTitle() verifySendToDeviceTitle()

@ -17,6 +17,7 @@ import org.junit.Before
import org.junit.Ignore import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -35,12 +36,13 @@ import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.tabDrawer import org.mozilla.fenix.ui.robots.tabDrawer
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/** /**
* Test Suite that contains tests defined as part of the Smoke and Sanity check defined in Test rail. * Test Suite that contains tests defined as part of the Smoke and Sanity check defined in Test rail.
* These tests will verify different functionalities of the app as a way to quickly detect regressions in main areas * These tests will verify different functionalities of the app as a way to quickly detect regressions in main areas
*/ */
@Suppress("ForbiddenComment")
class SmokeTest { class SmokeTest {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
@ -125,13 +127,9 @@ class SmokeTest {
verifyStartSyncHeader() verifyStartSyncHeader()
verifyAccountsSignInButton() verifyAccountsSignInButton()
// Intro to other sections // Always-on privacy
verifyGetToKnowHeader() scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
// Automatic privacy
scrollToElementByText("Automatic privacy")
verifyAutomaticPrivacyHeader() verifyAutomaticPrivacyHeader()
verifyTrackingProtectionToggle()
verifyAutomaticPrivacyText() verifyAutomaticPrivacyText()
// Choose your theme // Choose your theme
@ -142,11 +140,7 @@ class SmokeTest {
verifyLightThemeDescription() verifyLightThemeDescription()
verifyLightThemeToggle() verifyLightThemeToggle()
// Browse privately // Pick your toolbar placement
verifyBrowsePrivatelyHeader()
verifyBrowsePrivatelyText()
// Take a position
verifyTakePositionHeader() verifyTakePositionHeader()
verifyTakePositionElements() verifyTakePositionElements()
@ -161,6 +155,7 @@ class SmokeTest {
} }
@Test @Test
@Ignore("https://github.com/mozilla-mobile/fenix/issues/18603")
// Verifies the functionality of the onboarding Start Browsing button // Verifies the functionality of the onboarding Start Browsing button
fun startBrowsingButtonTest() { fun startBrowsingButtonTest() {
homeScreen { homeScreen {
@ -199,14 +194,13 @@ class SmokeTest {
@Test @Test
// Verifies the list of items in a tab's 3 dot menu // Verifies the list of items in a tab's 3 dot menu
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun verifyPageMainMenuItemsTest() { fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMainMenuItems() verifyPageThreeDotMainMenuItems()
} }
} }
@ -233,12 +227,21 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Synced tabs menu opens from a tab's 3 dot menu // Verifies the Synced tabs menu or Sync Sign In menu opens from a tab's 3 dot menu.
fun openMainMenuSyncedTabsItemTest() { // The test is assuming we are NOT signed in.
homeScreen { fun openMainMenuSyncItemTest() {
}.openThreeDotMenu { if (FeatureFlags.tabsTrayRewrite) {
}.openSyncedTabs { homeScreen {
verifySyncedTabsMenuHeader() }.openThreeDotMenu {
}.openSyncSignIn {
verifySyncSignInMenuHeader()
}
} else {
homeScreen {
}.openThreeDotMenu {
}.openSyncedTabs {
verifySyncedTabsMenuHeader()
}
} }
} }
@ -274,6 +277,7 @@ class SmokeTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
expandMenu()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
@ -292,12 +296,14 @@ class SmokeTest {
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) { }.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen { }.openAddToHomeScreen {
clickCancelShortcutButton() clickCancelShortcutButton()
} }
browserScreen { browserScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen { }.openAddToHomeScreen {
verifyShortcutNameField("Test_Page_1") verifyShortcutNameField("Test_Page_1")
addShortcutName("Test Page") addShortcutName("Test Page")
@ -322,7 +328,6 @@ class SmokeTest {
@Test @Test
// Verifies the Bookmark button in a tab's 3 dot menu // Verifies the Bookmark button in a tab's 3 dot menu
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
fun mainMenuBookmarkButtonTest() { fun mainMenuBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -357,7 +362,6 @@ class SmokeTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMenuExists() verifyThreeDotMenuExists()
verifyRefreshButton()
}.refreshPage { }.refreshPage {
verifyPageContent("REFRESHED") verifyPageContent("REFRESHED")
} }
@ -365,7 +369,6 @@ class SmokeTest {
@Test @Test
// Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar // Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun verifyETPShieldNotDisplayedIfOFFGlobally() { fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -392,6 +395,7 @@ class SmokeTest {
} }
} }
@Ignore("Failing, see https://github.com/mozilla-mobile/fenix/issues/18647")
@Test @Test
fun customTrackingProtectionSettingsTest() { fun customTrackingProtectionSettingsTest() {
val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) val trackingPage = TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
@ -542,7 +546,6 @@ class SmokeTest {
@Test @Test
// Saves a login, then changes it and verifies the update // Saves a login, then changes it and verifies the update
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun updateSavedLoginTest() { fun updateSavedLoginTest() {
val saveLoginTest = val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer) TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -606,7 +609,6 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
// Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers // Installs uBlock add-on and checks that the app doesn't crash while loading pages with trackers
fun noCrashWithAddonInstalledTest() { fun noCrashWithAddonInstalledTest() {
// setting ETP to Strict mode to test it works with add-ons // setting ETP to Strict mode to test it works with add-ons
@ -894,6 +896,7 @@ class SmokeTest {
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
fun verifyExpandedCollectionItemsTest() { fun verifyExpandedCollectionItemsTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -943,6 +946,7 @@ class SmokeTest {
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
fun shareCollectionTest() { fun shareCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -960,6 +964,7 @@ class SmokeTest {
} }
} }
@Ignore("Disabling until re-implemented by #19090")
@Test @Test
fun deleteCollectionTest() { fun deleteCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -981,7 +986,6 @@ class SmokeTest {
@Test @Test
// Verifies that deleting a Bookmarks folder also removes the item from inside it. // Verifies that deleting a Bookmarks folder also removes the item from inside it.
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
fun deleteNonEmptyBookmarkFolderTest() { fun deleteNonEmptyBookmarkFolderTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1) val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -1058,6 +1062,7 @@ class SmokeTest {
} }
} }
@Ignore("Feature is temporarily removed; disabling test. See https://github.com/mozilla-mobile/fenix/issues/18656")
@Test @Test
fun selectTabsButtonVisibilityTest() { fun selectTabsButtonVisibilityTest() {
homeScreen { homeScreen {
@ -1142,7 +1147,6 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17799")
fun mainMenuInstallPWATest() { fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/" val pwaPage = "https://rpappalax.github.io/testapp/"
@ -1159,7 +1163,6 @@ class SmokeTest {
} }
@Test @Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17971")
// Verifies that reader mode is detected and the custom appearance controls are displayed // Verifies that reader mode is detected and the custom appearance controls are displayed
fun verifyReaderViewAppearanceUI() { fun verifyReaderViewAppearanceUI() {
val readerViewPage = val readerViewPage =

@ -8,7 +8,6 @@ import androidx.test.platform.app.InstrumentationRegistry
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -127,7 +126,6 @@ class StrictEnhancedTrackingProtectionTest {
} }
@Test @Test
@Ignore("To be re-implemented with the three dot menu changes https://github.com/mozilla-mobile/fenix/issues/17870")
fun testStrictVisitDisable() { fun testStrictVisitDisable() {
val trackingProtectionTest = val trackingProtectionTest =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer) TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
@ -151,7 +149,6 @@ class StrictEnhancedTrackingProtectionTest {
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyThreeDotMenuExists() verifyThreeDotMenuExists()
verifySettingsButton()
}.openSettings { }.openSettings {
verifyEnhancedTrackingProtectionButton() verifyEnhancedTrackingProtectionButton()
verifyEnhancedTrackingProtectionValue("On") verifyEnhancedTrackingProtectionValue("On")

@ -4,12 +4,9 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
@ -38,85 +35,58 @@ class ThreeDotMenuMainTest {
} }
} }
// changing the device preference for Touch and Hold delay, to avoid long-clicks instead of a single-click
companion object {
@BeforeClass
@JvmStatic
fun setDevicePreference() {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.executeShellCommand("settings put secure long_press_timeout 3000")
}
}
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
} }
// Verifies the list of items in the homescreen's 3 dot main menu
@Test @Test
fun threeDotMenuItemsTest() { fun homeThreeDotMenuItemsTest() {
if (FeatureFlags.toolbarMenuFeature) { homeScreen {
homeScreen { }.openThreeDotMenu {
}.openThreeDotMenu { verifyBookmarksButton()
}.openHistory { verifyHistoryButton()
verifyHistoryMenuView() verifyDownloadsButton()
}.goBackToBrowser {} verifyAddOnsButton()
if (FeatureFlags.tabsTrayRewrite) {
homeScreen { verifySyncSignInButton()
}.openThreeDotMenu { } else {
}.openBookmarks { verifySyncedTabsButton()
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 {
}.openWhatsNew {
verifyWhatsNewURL()
}.openTabDrawer {
closeTab()
} }
verifyDesktopSite()
verifyWhatsNewButton()
verifyHelpButton()
verifySettingsButton()
}.openSettings {
verifySettingsView()
}.goBack {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.openTabDrawer {
closeTab()
}
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openBookmarks { }.openWhatsNew {
verifyBookmarksMenuView() verifyWhatsNewURL()
}.closeMenu { }.openTabDrawer {
} closeTab()
}
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openHistory { }.openBookmarks {
verifyHistoryMenuView() verifyBookmarksMenuView()
} }.closeMenu {
} }
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}
} }
} }

@ -54,7 +54,8 @@ class TopSitesTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddFirefoxHome() expandMenu()
verifyAddToTopSitesButton()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
@ -73,7 +74,8 @@ class TopSitesTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddFirefoxHome() expandMenu()
verifyAddToTopSitesButton()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
@ -104,7 +106,8 @@ class TopSitesTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddFirefoxHome() expandMenu()
verifyAddToTopSitesButton()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
@ -128,7 +131,8 @@ class TopSitesTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddFirefoxHome() expandMenu()
verifyAddToTopSitesButton()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {
@ -152,7 +156,8 @@ class TopSitesTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyAddFirefoxHome() expandMenu()
verifyAddToTopSitesButton()
}.addToFirefoxHome { }.addToFirefoxHome {
verifySnackBarText("Added to top sites!") verifySnackBarText("Added to top sites!")
}.openTabDrawer { }.openTabDrawer {

@ -310,8 +310,7 @@ class BrowserRobot {
// needs to wait for the right url to load before saving a bookmark // needs to wait for the right url to load before saving a bookmark
verifyUrl(url.toString()) verifyUrl(url.toString())
}.openThreeDotMenu { }.openThreeDotMenu {
clickAddBookmarkButton() }.bookmarkPage { }
}
} }
fun clickLinkMatchingText(expectedText: String) { fun clickLinkMatchingText(expectedText: String) {

@ -54,6 +54,9 @@ import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.helpers.matchers.hasItem import org.mozilla.fenix.helpers.matchers.hasItem
import org.mozilla.fenix.helpers.withBitmapDrawable import org.mozilla.fenix.helpers.withBitmapDrawable
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER
import org.mozilla.fenix.ui.util.STRING_ONBOARDING_TRACKING_PROTECTION_HEADER
/** /**
* Implementation of Robot Pattern for the home screen menu. * Implementation of Robot Pattern for the home screen menu.
@ -85,7 +88,6 @@ class HomeScreenRobot {
fun verifyStartSyncHeader() = assertStartSyncHeader() fun verifyStartSyncHeader() = assertStartSyncHeader()
fun verifyAccountsSignInButton() = assertAccountsSignInButton() fun verifyAccountsSignInButton() = assertAccountsSignInButton()
fun verifyGetToKnowHeader() = assertGetToKnowHeader()
fun verifyChooseThemeHeader() = assertChooseThemeHeader() fun verifyChooseThemeHeader() = assertChooseThemeHeader()
fun verifyChooseThemeText() = assertChooseThemeText() fun verifyChooseThemeText() = assertChooseThemeText()
fun verifyLightThemeToggle() = assertLightThemeToggle() fun verifyLightThemeToggle() = assertLightThemeToggle()
@ -95,18 +97,13 @@ class HomeScreenRobot {
fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle() fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle()
fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription() fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription()
fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader() fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader()
fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle() fun verifyAutomaticPrivacyText() = assertAlwaysPrivacyText()
fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText()
// Browse privately // Pick your toolbar placement
fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader() fun verifyTakePositionHeader() = assertTakePlacementHeader()
fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText()
// Take a position
fun verifyTakePositionHeader() = assertTakePositionheader()
fun verifyTakePositionElements() { fun verifyTakePositionElements() {
assertTakePositionBottomRadioButton() assertTakePlacementBottomRadioButton()
assertTakePositionTopRadioButton() assertTakePacementTopRadioButton()
} }
// Your privacy // Your privacy
@ -549,18 +546,15 @@ private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to ${appContext.appName}!"))) onView(allOf(withText("Welcome to ${appContext.appName}!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartSyncHeader() = private fun assertStartSyncHeader() {
onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account."))) scrollToElementByText(STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER)
onView(allOf(withText(R.string.onboarding_account_sign_in_header_1)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAccountsSignInButton() = private fun assertAccountsSignInButton() =
onView(ViewMatchers.withResourceName("fxa_sign_in_button")) onView(ViewMatchers.withResourceName("fxa_sign_in_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() =
onView(allOf(withText("Get to know ${appContext.appName}")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertChooseThemeHeader() { private fun assertChooseThemeHeader() {
scrollToElementByText("Choose your theme") scrollToElementByText("Choose your theme")
onView(withText("Choose your theme")) onView(withText("Choose your theme"))
@ -568,7 +562,7 @@ private fun assertChooseThemeHeader() {
} }
private fun assertChooseThemeText() { private fun assertChooseThemeText() {
scrollToElementByText("Choose your theme") scrollToElementByText("Choose your theme")
onView(allOf(withText("Save some battery and your eyesight by enabling dark mode."))) onView(allOf(withText("Save some battery and your eyesight with dark mode.")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
@ -608,40 +602,23 @@ private fun assertAutomaticThemeDescription() {
} }
private fun assertAutomaticPrivacyHeader() { private fun assertAutomaticPrivacyHeader() {
scrollToElementByText("Automatic privacy") scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(allOf(withText("Automatic privacy"))) onView(allOf(withText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertTrackingProtectionToggle() { private fun assertAlwaysPrivacyText() {
scrollToElementByText("Automatic privacy") scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(withId(R.id.tracking_protection_toggle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyText() {
scrollToElementByText("Automatic privacy")
onView( onView(
allOf( allOf(
withText( withText(
"Privacy and security settings block trackers, malware, and companies that follow you." R.string.onboarding_tracking_protection_description_3
) )
) )
) )
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertBrowsePrivatelyHeader() {
scrollToElementByText("Browse privately")
onView(allOf(withText("Browse privately")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertBrowsePrivatelyText() {
scrollToElementByText("Browse privately")
onView(allOf(withText(containsString("Update your private browsing settings."))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertYourPrivacyHeader() { private fun assertYourPrivacyHeader() {
scrollToElementByText("Your privacy") scrollToElementByText("Your privacy")
onView(allOf(withText("Your privacy"))) onView(allOf(withText("Your privacy")))
@ -668,25 +645,25 @@ private fun assertPrivacyNoticeButton() {
private fun assertStartBrowsingButton() { private fun assertStartBrowsingButton() {
scrollToElementByText("Start browsing") scrollToElementByText("Start browsing")
onView(allOf(withText("Start browsing"))) onView(withId(R.id.finish_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
// Take a position // Pick your toolbar placement
private fun assertTakePositionheader() { private fun assertTakePlacementHeader() {
scrollToElementByText("Take a position") scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(allOf(withText("Take a position"))) onView(allOf(withText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertTakePositionTopRadioButton() { private fun assertTakePacementTopRadioButton() {
scrollToElementByText("Take a position") scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_top_radio_button")) onView(ViewMatchers.withResourceName("toolbar_top_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun assertTakePositionBottomRadioButton() { private fun assertTakePlacementBottomRadioButton() {
scrollToElementByText("Take a position") scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button")) onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }

@ -109,8 +109,7 @@ class NavigationToolbarRobot {
withResourceName("onboarding_message"), // Req ETP dialog withResourceName("onboarding_message"), // Req ETP dialog
withResourceName("download_button") withResourceName("download_button")
) )
) ).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
} }
BrowserRobot().interact() BrowserRobot().interact()
@ -186,10 +185,10 @@ class NavigationToolbarRobot {
} }
fun openTabTray(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabTray(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
onView(withId(R.id.tab_button)) mDevice.waitForIdle(waitingTime)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
tabTrayButton().click() tabTrayButton().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() return TabDrawerRobot.Transition()

@ -269,16 +269,20 @@ private fun assertThemeSelected() = onView(withText("Light"))
private fun assertAccessibilityButton() = onView(withText("Accessibility")) private fun assertAccessibilityButton() = onView(withText("Accessibility"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertSetAsDefaultBrowserButton() = private fun assertSetAsDefaultBrowserButton() {
scrollToElementByText("Set as default browser")
onView(withText("Set as default browser")) onView(withText("Set as default browser"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertDefaultBrowserIsDisabled() { private fun assertDefaultBrowserIsDisabled() {
scrollToElementByText("Set as default browser")
onView(withId(R.id.switch_widget)) onView(withId(R.id.switch_widget))
.check(matches(ViewMatchers.isNotChecked())) .check(matches(ViewMatchers.isNotChecked()))
} }
private fun toggleDefaultBrowserSwitch() { private fun toggleDefaultBrowserSwitch() {
scrollToElementByText("Set as default browser")
onView( onView(
CoreMatchers.allOf( CoreMatchers.allOf(
ViewMatchers.withParent(CoreMatchers.not(withId(R.id.navigationToolbar))), ViewMatchers.withParent(CoreMatchers.not(withId(R.id.navigationToolbar))),

@ -77,7 +77,7 @@ private fun assertDataCollectionOptions() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val marketingDataText = val marketingDataText =
"Shares data about what features you use in Firefox Preview with Leanplum, our mobile marketing vendor." "Shares basic usage data with Adjust, our mobile marketing vendor"
onView(withText(marketingDataText)) onView(withText(marketingDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

@ -143,16 +143,13 @@ private fun assertEnhancedTrackingProtectionOptions() {
onView(withText("Standard (default)")) onView(withText("Standard (default)"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val stdText = "Blocks fewer trackers. Pages will load normally." onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_standard_description_4))
onView(withText(stdText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Strict")) onView(withText("Strict"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
val strictText = onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_strict_description_3))
"Blocks more trackers, ads, and popups. Pages load faster, but some functionality might not work."
onView(withText(strictText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText("Custom")) onView(withText("Custom"))
@ -168,16 +165,13 @@ private fun assertEnhancedTrackingProtectionOptionsGrayedOut() {
onView(withText("Standard (default)")) onView(withText("Standard (default)"))
.check(matches(not(isEnabled(true)))) .check(matches(not(isEnabled(true))))
val stdText = "Blocks fewer trackers. Pages will load normally." onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_standard_description_4))
onView(withText(stdText))
.check(matches(not(isEnabled(true)))) .check(matches(not(isEnabled(true))))
onView(withText("Strict")) onView(withText("Strict"))
.check(matches(not(isEnabled(true)))) .check(matches(not(isEnabled(true))))
val strictText = onView(withText(org.mozilla.fenix.R.string.preference_enhanced_tracking_protection_strict_description_3))
"Blocks more trackers, ads, and popups. Pages load faster, but some functionality might not work."
onView(withText(strictText))
.check(matches(not(isEnabled(true)))) .check(matches(not(isEnabled(true))))
onView(withText("Custom")) onView(withText("Custom"))

@ -129,12 +129,19 @@ private fun assertVideoAndAudioBlockedRecommended() = onView(withId(R.id.fourth_
private fun assertCheckAutoPayRadioButtonDefault() { private fun assertCheckAutoPayRadioButtonDefault() {
// Allow audio and video
onView(withId(R.id.block_radio)) onView(withId(R.id.block_radio))
.assertIsChecked(isChecked = false) .assertIsChecked(isChecked = false)
// Block audio and video on cellular data only
onView(withId(R.id.block_radio))
.assertIsChecked(isChecked = false)
// Block audio only
onView(withId(R.id.third_radio)) onView(withId(R.id.third_radio))
.assertIsChecked(isChecked = false) .assertIsChecked(isChecked = false)
// Block audio and video
onView(withId(R.id.fourth_radio)) onView(withId(R.id.fourth_radio))
.assertIsChecked(isChecked = true) .assertIsChecked(isChecked = true)
} }

@ -0,0 +1,52 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui.robots
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for Sync Sign In sub menu.
*/
class SyncSignInRobot {
fun verifyAccountSettingsMenuHeader() = assertAccountSettingsMenuHeader()
fun verifySyncSignInMenuHeader() = assertSyncSignInMenuHeader()
class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())!!
fun goBack(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
goBackButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up")))
private fun assertAccountSettingsMenuHeader() {
// Replaced with the new string here, the test is assuming we are NOT signed in
// Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync
onView(withText(R.string.preferences_account_settings))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}
private fun assertSyncSignInMenuHeader() {
onView(withText(R.string.sign_in_with_camera))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
}

@ -39,6 +39,8 @@ private fun goBackButton() =
onView(allOf(withContentDescription("Navigate up"))) onView(allOf(withContentDescription("Navigate up")))
private fun assertSyncedTabsMenuHeader() { private fun assertSyncedTabsMenuHeader() {
onView(withText(R.string.synced_tabs)) // Replaced with the new string here, the test is assuming we are NOT signed in
// Sync tests in SettingsSyncTest are still TO-DO, so I'm not sure that we have a test for signing into Sync
onView(withText(R.string.sync_menu_sign_in))
.check((matches(withEffectiveVisibility(Visibility.VISIBLE)))) .check((matches(withEffectiveVisibility(Visibility.VISIBLE))))
} }

@ -160,11 +160,29 @@ class TabDrawerRobot {
fun clickTabMediaControlButton() = tabMediaControlButton().click() fun clickTabMediaControlButton() = tabMediaControlButton().click()
fun clickSelectTabs() = onView(withText("Select tabs")).click() fun clickSelectTabs() {
threeDotMenu().click()
mDevice.waitNotNull(
Until.findObject(text("Select tabs")),
waitingTime
)
val selectTabsButton = mDevice.findObject(text("Select tabs"))
selectTabsButton.click()
}
fun clickAddNewCollection() = addNewCollectionButton().click() fun clickAddNewCollection() = addNewCollectionButton().click()
fun selectTab(title: String) = tab(title).click() fun selectTab(title: String) {
mDevice.waitNotNull(
findObject(text(title)),
waitingTime
)
val tab = mDevice.findObject(text(title))
tab.click()
}
fun clickSaveCollection() = saveTabsToCollectionButton().click() fun clickSaveCollection() = saveTabsToCollectionButton().click()
@ -198,15 +216,10 @@ class TabDrawerRobot {
} }
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/tab_button")) mDevice.waitForIdle(waitingTime)
.waitForExists(waitingTime)
tabsCounter().click() tabsCounter().click()
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
org.mozilla.fenix.ui.robots.mDevice.waitNotNull( waitingTime)
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")),
waitingTime
)
TabDrawerRobot().interact() TabDrawerRobot().interact()
return TabDrawerRobot.Transition() return TabDrawerRobot.Transition()

@ -6,24 +6,17 @@
package org.mozilla.fenix.ui.robots package org.mozilla.fenix.ui.robots
import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.Espresso.pressBack
import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.swipeDown import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
@ -35,10 +28,9 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -48,6 +40,7 @@ import org.mozilla.fenix.share.ShareFragment
/** /**
* Implementation of Robot Pattern for the three dot (main) menu. * Implementation of Robot Pattern for the three dot (main) menu.
*/ */
@Suppress("ForbiddenComment")
class ThreeDotMenuMainRobot { class ThreeDotMenuMainRobot {
fun verifyTabSettingsButton() = assertTabSettingsButton() fun verifyTabSettingsButton() = assertTabSettingsButton()
fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton() fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton()
@ -58,6 +51,7 @@ class ThreeDotMenuMainRobot {
fun verifyHistoryButton() = assertHistoryButton() fun verifyHistoryButton() = assertHistoryButton()
fun verifyBookmarksButton() = assertBookmarksButton() fun verifyBookmarksButton() = assertBookmarksButton()
fun verifySyncedTabsButton() = assertSyncedTabsButton() fun verifySyncedTabsButton() = assertSyncedTabsButton()
fun verifySyncSignInButton() = assertSignInToSyncButton()
fun verifyHelpButton() = assertHelpButton() fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists() fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyForwardButton() = assertForwardButton() fun verifyForwardButton() = assertForwardButton()
@ -68,7 +62,16 @@ class ThreeDotMenuMainRobot {
fun verifyShareButton() = assertShareButton() fun verifyShareButton() = assertShareButton()
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
fun expandMenu() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
}
fun clickShareButton() { fun clickShareButton() {
var maxSwipes = 3
while (!shareButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
shareButton().click() shareButton().click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
} }
@ -85,73 +88,41 @@ class ThreeDotMenuMainRobot {
addNewCollectionButton().click() addNewCollectionButton().click()
} }
fun clickAddBookmarkButton() {
mDevice.waitNotNull(
Until.findObject(By.desc("Bookmark")),
waitingTime
)
addBookmarkButton().perform(
click(
/* no-op rollback action for when clicks randomly perform a long click, Espresso should attempt to click again
https://issuetracker.google.com/issues/37078920#comment9
*/
object : ViewAction {
override fun getDescription(): String {
return "Handle tap->longclick."
}
override fun getConstraints(): Matcher<View> {
return isAssignableFrom(View::class.java)
}
override fun perform(uiController: UiController?, view: View?) {
// do nothing
}
}
)
)
}
fun verifyCollectionNameTextField() = assertCollectionNameTextField() fun verifyCollectionNameTextField() = assertCollectionNameTextField()
fun verifyFindInPageButton() = assertFindInPageButton() fun verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim() fun verifyShareScrim() = assertShareScrim()
fun verifySendToDeviceTitle() = assertSendToDeviceTitle() fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
fun verifyShareALinkTitle() = assertShareALinkTitle() fun verifyShareALinkTitle() = assertShareALinkTitle()
fun verifyWhatsNewButton() = assertWhatsNewButton() fun verifyWhatsNewButton() = assertWhatsNewButton()
fun verifyAddFirefoxHome() = assertAddToFirefoxHome() fun verifyAddToTopSitesButton() = assertAddToTopSitesButton()
fun verifyAddToMobileHome() = assertAddToMobileHome() fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite() fun verifyDesktopSite() = assertDesktopSite()
fun verifyDownloadsButton() = assertDownloadsButton() fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifySignInToSyncButton() = assertSignInToSyncButton()
fun verifyThreeDotMainMenuItems() { fun verifyNewTabButton() = assertNewTabButton()
if (FeatureFlags.toolbarMenuFeature) { fun verifyReportSiteIssueButton() = assertReportSiteIssueButton()
verifyDownloadsButton()
verifyHistoryButton() fun verifyPageThreeDotMainMenuItems() {
verifyBookmarksButton() verifyNewTabButton()
verifySettingsButton() verifyBookmarksButton()
verifyDesktopSite() verifyAddBookmarkButton()
verifySaveCollection() verifyHistoryButton()
verifyShareButton() verifyDownloadsButton()
verifyForwardButton() verifyAddOnsButton()
verifyRefreshButton() verifySignInToSyncButton()
} else { threeDotMenuRecyclerView().perform(swipeUp())
verifyAddOnsButton() verifyFindInPageButton()
verifyDownloadsButton() verifyDesktopSite()
verifyHistoryButton() threeDotMenuRecyclerView().perform(swipeUp())
verifyBookmarksButton() verifyReportSiteIssueButton()
verifySyncedTabsButton() verifyAddToTopSitesButton()
verifySettingsButton() verifyAddToMobileHome()
verifyFindInPageButton() verifySaveCollection()
verifyAddFirefoxHome() verifySettingsButton()
verifyAddToMobileHome() verifyShareButton()
verifyDesktopSite() verifyForwardButton()
verifySaveCollection() verifyRefreshButton()
verifyAddBookmarkButton()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
}
} }
private fun assertShareTabsOverlay() { private fun assertShareTabsOverlay() {
@ -166,11 +137,12 @@ class ThreeDotMenuMainRobot {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition { fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) var maxSwipes = 3
onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings))) while (!settingsButton().exists() && maxSwipes != 0) {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) threeDotMenuRecyclerView().perform(swipeUp())
.check(matches(isCompletelyDisplayed())) maxSwipes--
.perform(ViewActions.click()) }
settingsButton().click()
SettingsRobot().interact() SettingsRobot().interact()
return SettingsRobot.Transition() return SettingsRobot.Transition()
@ -185,7 +157,7 @@ class ThreeDotMenuMainRobot {
} }
fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition { fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime)
syncedTabsButton().click() syncedTabsButton().click()
@ -193,6 +165,15 @@ class ThreeDotMenuMainRobot {
return SyncedTabsRobot.Transition() return SyncedTabsRobot.Transition()
} }
fun openSyncSignIn(interact: SyncSignInRobot.() -> Unit): SyncSignInRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Sign in to sync")), waitingTime)
signInToSyncButton().click()
SyncSignInRobot().interact()
return SyncSignInRobot.Transition()
}
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition { fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
@ -205,7 +186,7 @@ class ThreeDotMenuMainRobot {
} }
fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition { fun openHistory(interact: HistoryRobot.() -> Unit): HistoryRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("History")), waitingTime)
historyButton().click() historyButton().click()
@ -214,7 +195,7 @@ class ThreeDotMenuMainRobot {
} }
fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun bookmarkPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Bookmark")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
addBookmarkButton().click() addBookmarkButton().click()
BrowserRobot().interact() BrowserRobot().interact()
@ -222,10 +203,7 @@ class ThreeDotMenuMainRobot {
} }
fun sharePage(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition { fun sharePage(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Share")), waitingTime)
shareButton().click() shareButton().click()
pressBack()
LibrarySubMenusMultipleSelectionToolbarRobot().interact() LibrarySubMenusMultipleSelectionToolbarRobot().interact()
return LibrarySubMenusMultipleSelectionToolbarRobot.Transition() return LibrarySubMenusMultipleSelectionToolbarRobot.Transition()
} }
@ -239,7 +217,6 @@ class ThreeDotMenuMainRobot {
} }
fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Forward")), waitingTime)
forwardButton().click() forwardButton().click()
BrowserRobot().interact() BrowserRobot().interact()
@ -273,7 +250,7 @@ class ThreeDotMenuMainRobot {
} }
fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Refresh")), waitingTime) assertRefreshButton()
refreshButton().click() refreshButton().click()
BrowserRobot().interact() BrowserRobot().interact()
@ -303,7 +280,7 @@ class ThreeDotMenuMainRobot {
} }
fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition { fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
findInPageButton().click() findInPageButton().click()
@ -340,6 +317,11 @@ class ThreeDotMenuMainRobot {
} }
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition { fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
var maxSwipes = 3
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
readerViewAppearanceToggle().click() readerViewAppearanceToggle().click()
ReaderViewRobot().interact() ReaderViewRobot().interact()
@ -347,7 +329,7 @@ class ThreeDotMenuMainRobot {
} }
fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
addToFirefoxHomeButton().click() addToTopSitesButton().click()
BrowserRobot().interact() BrowserRobot().interact()
return BrowserRobot.Transition() return BrowserRobot.Transition()
@ -362,6 +344,11 @@ class ThreeDotMenuMainRobot {
} }
fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition { fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
var maxSwipes = 3
while (!installPWAButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
installPWAButton().click() installPWAButton().click()
AddToHomeScreenRobot().interact() AddToHomeScreenRobot().interact()
@ -377,6 +364,11 @@ class ThreeDotMenuMainRobot {
} }
fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
// Ensure the menu is expanded and fully scrolled to the bottom.
for (i in 0..3) {
threeDotMenuRecyclerView().perform(swipeUp())
}
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click() saveCollectionButton().click()
ThreeDotMenuMainRobot().interact() ThreeDotMenuMainRobot().interact()
@ -401,18 +393,17 @@ class ThreeDotMenuMainRobot {
} }
} }
} }
private fun threeDotMenuRecyclerView() =
onView(withId(R.id.mozac_browser_menu_recyclerView))
private fun threeDotMenuRecyclerViewExists() { private fun threeDotMenuRecyclerViewExists() {
onView(withId(R.id.mozac_browser_menu_recyclerView)).check(matches(isDisplayed())) threeDotMenuRecyclerView().check(matches(isDisplayed()))
} }
private fun settingsButton() = onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings))) private fun settingsButton() = mDevice.findObject(UiSelector().text("Settings"))
private fun assertSettingsButton() = settingsButton() private fun assertSettingsButton() = assertTrue(settingsButton().waitForExists(waitingTime))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isCompletelyDisplayed()))
private val addOnsText = if (FeatureFlags.toolbarMenuFeature) "Extensions" else "Add-ons" private fun addOnsButton() = onView(allOf(withText("Add-ons")))
private fun addOnsButton() = onView(allOf(withText(addOnsText)))
private fun assertAddOnsButton() { private fun assertAddOnsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) addOnsButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
@ -430,27 +421,28 @@ private fun syncedTabsButton() = onView(allOf(withText(R.string.library_synced_t
private fun assertSyncedTabsButton() = syncedTabsButton() private fun assertSyncedTabsButton() = syncedTabsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun signInToSyncButton() = onView(withText("Sign in to sync"))
private fun assertSignInToSyncButton() = signInToSyncButton().check(matches(isDisplayed()))
private fun helpButton() = onView(allOf(withText(R.string.browser_menu_help))) private fun helpButton() = onView(allOf(withText(R.string.browser_menu_help)))
private fun assertHelpButton() = helpButton() private fun assertHelpButton() = helpButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forward")) private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun assertForwardButton() = forwardButton() private fun assertForwardButton() = assertTrue(forwardButton().waitForExists(waitingTime))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark")) private fun addBookmarkButton() = onView(allOf(withId(R.id.checkbox), withText("Add")))
private fun assertAddBookmarkButton() { private fun assertAddBookmarkButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp()) onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE))) addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark")) private fun editBookmarkButton() = onView(withText("Edit"))
private fun assertEditBookmarkButton() = editBookmarkButton() private fun assertEditBookmarkButton() = editBookmarkButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh")) private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
private fun assertRefreshButton() = refreshButton() private fun assertRefreshButton() = assertTrue(refreshButton().waitForExists(waitingTime))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop")) private fun stopLoadingButton() = onView(ViewMatchers.withContentDescription("Stop"))
@ -462,14 +454,13 @@ private fun shareTabButton() = onView(allOf(withText("Share all tabs"))).inRoot(
private fun assertShareTabButton() = shareTabButton() private fun assertShareTabButton() = shareTabButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun shareButton() = onView(ViewMatchers.withContentDescription("Share")) private fun shareButton() = mDevice.findObject(UiSelector().description("Share"))
private fun assertShareButton() = shareButton() private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun browserViewSaveCollectionButton() = onView( private fun browserViewSaveCollectionButton() = onView(
allOf( allOf(
withText("Save to collection"), withText("Save to collection"),
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE) withEffectiveVisibility(Visibility.VISIBLE)
) )
) )
@ -492,11 +483,14 @@ private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun reportSiteIssueButton() = onView(withText("Report Site Issue…")) private fun reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun assertReportSiteIssueButton() = reportSiteIssueButton().check(matches(isDisplayed()))
private fun findInPageButton() = onView(allOf(withText("Find in page"))) private fun findInPageButton() = onView(allOf(withText("Find in page")))
private fun assertFindInPageButton() = findInPageButton() private fun assertFindInPageButton() = findInPageButton()
private fun shareScrim() = onView(withResourceName("closeSharingScrim")) private fun shareScrim() = onView(withResourceName("closeSharingScrim"))
private fun assertShareScrim() = private fun assertShareScrim() =
shareScrim().check(matches(ViewMatchers.withAlpha(ShareFragment.SHOW_PAGE_ALPHA))) shareScrim().check(matches(ViewMatchers.withAlpha(ShareFragment.SHOW_PAGE_ALPHA)))
@ -524,17 +518,29 @@ private fun assertWhatsNewButton() = whatsNewButton()
private fun addToHomeScreenButton() = onView(withText("Add to Home screen")) private fun addToHomeScreenButton() = onView(withText("Add to Home screen"))
private fun readerViewAppearanceToggle() = private fun readerViewAppearanceToggle() =
onView(allOf(withText(R.string.browser_menu_read_appearance))) mDevice.findObject(UiSelector().text("Customize reader view"))
private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppearanceToggle() private fun assertReaderViewAppearanceButton(visible: Boolean) {
.check( var maxSwipes = 3
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE)) else ViewAssertions.doesNotExist() if (visible) {
) while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
assertTrue(readerViewAppearanceToggle().exists())
} else {
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
assertFalse(readerViewAppearanceToggle().exists())
}
}
private fun addToFirefoxHomeButton() = private fun addToTopSitesButton() =
onView(allOf(withText(R.string.browser_menu_add_to_top_sites))) onView(allOf(withText(R.string.browser_menu_add_to_top_sites)))
private fun assertAddToFirefoxHome() { private fun assertAddToTopSitesButton() {
onView(withId(R.id.mozac_browser_menu_recyclerView)) onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform( .perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>( RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
@ -545,6 +551,7 @@ private fun assertAddToFirefoxHome() {
private fun addToMobileHomeButton() = private fun addToMobileHomeButton() =
onView(allOf(withText(R.string.browser_menu_add_to_homescreen))) onView(allOf(withText(R.string.browser_menu_add_to_homescreen)))
private fun assertAddToMobileHome() { private fun assertAddToMobileHome() {
onView(withId(R.id.mozac_browser_menu_recyclerView)) onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform( .perform(
@ -554,7 +561,7 @@ private fun assertAddToMobileHome() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun installPWAButton() = onView(allOf(withId(R.id.highlight_text), withText("Install"))) private fun installPWAButton() = mDevice.findObject(UiSelector().text("Install"))
private fun desktopSiteButton() = private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site))) onView(allOf(withText(R.string.browser_menu_desktop_site)))
@ -602,3 +609,5 @@ private fun assertShareAllTabsButton() {
.check( .check(
matches(isDisplayed())) matches(isDisplayed()))
} }
private fun assertNewTabButton() = onView(withText("New tab")).check(matches(isDisplayed()))

@ -0,0 +1,9 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.ui.util
const val STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER = "Sync Firefox between devices"
const val STRING_ONBOARDING_TRACKING_PROTECTION_HEADER = "Always-on privacy"
const val STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER = "Pick your toolbar placement"

@ -83,6 +83,7 @@ object GeckoProvider {
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage) val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
@Suppress("Deprecation")
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
return geckoRuntime return geckoRuntime

@ -83,6 +83,7 @@ object GeckoProvider {
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage) val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
@Suppress("Deprecation")
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
return geckoRuntime return geckoRuntime

@ -93,6 +93,7 @@ object GeckoProvider {
val geckoRuntime = GeckoRuntime.create(context, runtimeSettings) val geckoRuntime = GeckoRuntime.create(context, runtimeSettings)
val loginStorageDelegate = GeckoLoginStorageDelegate(storage) val loginStorageDelegate = GeckoLoginStorageDelegate(storage)
@Suppress("Deprecation")
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate) geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
return geckoRuntime return geckoRuntime

@ -25,6 +25,7 @@
<application <application
android:name=".FenixApplication" android:name=".FenixApplication"
android:allowBackup="false" android:allowBackup="false"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:requestLegacyExternalStorage="${requestLegacyExternalStorage}" android:requestLegacyExternalStorage="${requestLegacyExternalStorage}"

@ -15,6 +15,7 @@ import mozilla.components.concept.engine.request.RequestInterceptor
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.isOnline import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class AppRequestInterceptor( class AppRequestInterceptor(
@ -97,7 +98,7 @@ class AppRequestInterceptor(
// Navigate and trigger add-on installation. // Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId -> matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate( navController?.get()?.navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId) NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
) )

@ -31,6 +31,7 @@ enum class BrowserDirection(@IdRes val fragmentId: Int) {
FromAddonDetailsFragment(R.id.addonDetailsFragment), FromAddonDetailsFragment(R.id.addonDetailsFragment),
FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment), FromAddonPermissionsDetailsFragment(R.id.addonPermissionsDetailFragment),
FromLoginDetailFragment(R.id.loginDetailFragment), FromLoginDetailFragment(R.id.loginDetailFragment),
FromTabTray(R.id.tabTrayDialogFragment), FromTabTrayDialog(R.id.tabTrayDialogFragment),
FromTabTray(R.id.tabsTrayFragment),
FromRecentlyClosed(R.id.recentlyClosedFragment) FromRecentlyClosed(R.id.recentlyClosedFragment)
} }

@ -37,15 +37,10 @@ object FeatureFlags {
/** /**
* Shows new three-dot toolbar menu design. * Shows new three-dot toolbar menu design.
*/ */
val toolbarMenuFeature = Config.channel.isDebug const val toolbarMenuFeature = true
/** /**
* Enables the tabs tray re-write with Synced Tabs. * Enables the tabs tray re-write with Synced Tabs.
*/ */
val tabsTrayRewrite = Config.channel.isDebug const val tabsTrayRewrite = true
/**
* Enables the updated icon set look and feel.
*/
val newIconSet = Config.channel.isNightlyOrDebug
} }

@ -13,6 +13,7 @@ import androidx.annotation.CallSuper
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.ProcessLifecycleOwner
import androidx.work.Configuration.Builder import androidx.work.Configuration.Builder
import androidx.work.Configuration.Provider import androidx.work.Configuration.Provider
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
@ -59,6 +60,7 @@ import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.telemetry.TelemetryLifecycleObserver
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -190,8 +192,12 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
// runStorageMaintenance() // runStorageMaintenance()
// } // }
components.appStartReasonProvider.registerInAppOnCreate(this)
components.startupActivityLog.registerInAppOnCreate(this)
initVisualCompletenessQueueAndQueueTasks() initVisualCompletenessQueueAndQueueTasks()
ProcessLifecycleOwner.get().lifecycle.addObserver(TelemetryLifecycleObserver(components.core.store))
components.appStartupTelemetry.onFenixApplicationOnCreate() components.appStartupTelemetry.onFenixApplicationOnCreate()
} }
} }

@ -16,6 +16,7 @@ import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ActionMode
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.WindowManager.LayoutParams.FLAG_SECURE import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper import androidx.annotation.CallSuper
@ -32,13 +33,14 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
@ -77,13 +79,13 @@ import org.mozilla.fenix.exceptions.trackingprotection.TrackingProtectionExcepti
import org.mozilla.fenix.ext.alreadyOnDestination import org.mozilla.fenix.ext.alreadyOnDestination
import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.breadcrumb
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor import org.mozilla.fenix.home.intent.OpenSpecificTabIntentProcessor
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
@ -92,10 +94,13 @@ import org.mozilla.fenix.library.bookmarks.BookmarkFragmentDirections
import org.mozilla.fenix.library.bookmarks.DesktopFolders import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.NavGraphProvider
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers import org.mozilla.fenix.perf.ProfilerMarkers
import org.mozilla.fenix.perf.StartupPathProvider
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.session.PrivateNotificationService
import org.mozilla.fenix.settings.SettingsFragmentDirections import org.mozilla.fenix.settings.SettingsFragmentDirections
@ -107,6 +112,7 @@ import org.mozilla.fenix.settings.search.AddSearchEngineFragmentDirections
import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections import org.mozilla.fenix.settings.search.EditCustomSearchEngineFragmentDirections
import org.mozilla.fenix.share.AddNewDeviceFragmentDirections import org.mozilla.fenix.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager import org.mozilla.fenix.theme.DefaultThemeManager
@ -121,7 +127,7 @@ import java.lang.ref.WeakReference
* - browser screen * - browser screen
*/ */
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@SuppressWarnings("TooManyFunctions", "LargeClass") @SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity { open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL // DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// we need to store startup timestamp for warm startup. we cant directly store // we need to store startup timestamp for warm startup. we cant directly store
@ -129,7 +135,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// components requires context to access. // components requires context to access.
protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos() protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos()
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager lateinit var browsingModeManager: BrowsingModeManager
@ -154,7 +159,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
listOf( listOf(
SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics), SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics), StartSearchIntentProcessor(components.analytics.metrics),
DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService),
OpenBrowserIntentProcessor(this, ::getIntentSessionId), OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this) OpenSpecificTabIntentProcessor(this)
) )
@ -165,6 +169,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
private lateinit var navigationToolbar: Toolbar private lateinit var navigationToolbar: Toolbar
// Tracker for contextual menu (Copy|Search|Select all|etc...)
private var actionMode: ActionMode? = null
private val startupPathProvider = StartupPathProvider()
private lateinit var startupTypeTelemetry: StartupTypeTelemetry
final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measureNoInline { final override fun onCreate(savedInstanceState: Bundle?): Unit = PerfStartup.homeActivityOnCreate.measureNoInline {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL. // DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity") components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity")
@ -172,6 +182,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager) components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
// There is disk read violations on some devices such as samsung and pixel for android 9/10 // There is disk read violations on some devices such as samsung and pixel for android 9/10
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
// Theme setup should always be called before super.onCreate
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
@ -187,8 +199,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch() components.publicSuffixList.prefetch()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent)) setContentView(R.layout.activity_home).run {
setContentView(R.layout.activity_home) // Do not call anything between setContentView and inflateNavGraphAsync.
// It needs to start its job as early as possible.
NavGraphProvider.inflateNavGraphAsync(navHost.navController, lifecycleScope)
}
// Must be after we set the content view // Must be after we set the content view
if (isVisuallyComplete) { if (isVisuallyComplete) {
@ -204,17 +219,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start() it.start()
} }
if (isActivityColdStarted( if (isActivityColdStarted(intent, savedInstanceState) &&
intent, !externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }) {
savedInstanceState
) && !externalSourceIntentProcessors.any {
it.process(
intent,
navHost.navController,
this.intent
)
}
) {
navigateToBrowserOnColdStart() navigateToBrowserOnColdStart()
} }
@ -252,6 +258,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics() captureSnapshotTelemetryMetrics()
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null) startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnHomeActivityOnCreate(lifecycle)
}
components.core.requestInterceptor.setNavigationController(navHost.navController) components.core.requestInterceptor.setNavigationController(navHost.navController)
@ -262,11 +272,19 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
safeIntent: SafeIntent, safeIntent: SafeIntent,
hasSavedInstanceState: Boolean hasSavedInstanceState: Boolean
) { ) {
// This function gets overridden by subclasses.
components.appStartupTelemetry.onHomeActivityOnCreate( components.appStartupTelemetry.onHomeActivityOnCreate(
safeIntent, safeIntent,
hasSavedInstanceState, hasSavedInstanceState,
homeActivityInitTimeStampNanoSeconds, rootContainer homeActivityInitTimeStampNanoSeconds, rootContainer
) )
components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate(
components.performance.visualCompletenessQueue,
components.startupStateProvider,
safeIntent,
rootContainer
)
} }
override fun onRestart() { override fun onRestart() {
@ -455,6 +473,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
intent?.let { intent?.let {
handleNewIntent(it) handleNewIntent(it)
} }
startupPathProvider.onIntentReceived(intent)
} }
open fun handleNewIntent(intent: Intent) { open fun handleNewIntent(intent: Intent) {
@ -519,6 +538,20 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
else -> super.onCreateView(parent, name, context, attrs) else -> super.onCreateView(parent, name, context, attrs)
} }
override fun onActionModeStarted(mode: ActionMode?) {
actionMode = mode
super.onActionModeStarted(mode)
}
override fun onActionModeFinished(mode: ActionMode?) {
actionMode = null
super.onActionModeFinished(mode)
}
fun finishActionMode() {
actionMode?.finish().also { actionMode = null }
}
@Suppress("MagicNumber") @Suppress("MagicNumber")
// Defining the positions as constants doesn't seem super useful here. // Defining the positions as constants doesn't seem super useful here.
private fun actionSorter(actions: Array<String>): Array<String> { private fun actionSorter(actions: Array<String>): Array<String> {
@ -721,10 +754,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
customTabSessionId: String? = null, customTabSessionId: String? = null,
engine: SearchEngine? = null, engine: SearchEngine? = null,
forceSearch: Boolean = false, forceSearch: Boolean = false,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none() flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
requestDesktopMode: Boolean = false
) { ) {
openToBrowser(from, customTabSessionId) openToBrowser(from, customTabSessionId)
load(searchTermOrURL, newTab, engine, forceSearch, flags) load(searchTermOrURL, newTab, engine, forceSearch, flags, requestDesktopMode)
} }
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) { fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
@ -774,8 +808,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId) AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment -> BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId) LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray -> BrowserDirection.FromTabTrayDialog ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId) TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed -> BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId) RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
} }
@ -790,7 +826,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
newTab: Boolean, newTab: Boolean,
engine: SearchEngine?, engine: SearchEngine?,
forceSearch: Boolean, forceSearch: Boolean,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none() flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
requestDesktopMode: Boolean = false
) { ) {
val startTime = components.core.engine.profiler?.getProfilerTime() val startTime = components.core.engine.profiler?.getProfilerTime()
val mode = browsingModeManager.mode val mode = browsingModeManager.mode
@ -807,6 +844,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// and let it try to load whatever was entered. // and let it try to load whatever was entered.
if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) { if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags) loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
if (requestDesktopMode) {
handleRequestDesktopMode()
}
} else { } else {
if (newTab) { if (newTab) {
components.useCases.searchUseCases.newTabSearch components.useCases.searchUseCases.newTabSearch
@ -833,6 +874,19 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
internal fun handleRequestDesktopMode() {
val requestDesktopSiteUseCase =
components.useCases.sessionUseCases.requestDesktopSite
requestDesktopSiteUseCase.invoke(true)
components.core.store.dispatch(
ContentAction.UpdateDesktopModeAction(
components.core.store.state.selectedTabId.toString(), true
)
)
// Reset preference value after opening the tab in desktop mode
settings().openNextTabInDesktopMode = false
}
open fun navigateToBrowserOnColdStart() { open fun navigateToBrowserOnColdStart() {
// Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last // Normal tabs + cold start -> Should go back to browser if we had any tabs open when we left last
// except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate // except for PBM + Cold Start there won't be any tabs since they're evicted so we never will navigate
@ -850,7 +904,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
override fun getSystemService(name: String): Any? { override fun getSystemService(name: String): Any? {
if (LAYOUT_INFLATER_SERVICE == name) { // Issue #17759 had a crash with the PerformanceInflater.kt on Android 5.0 and 5.1
// when using the TimePicker. Since the inflater was created for performance monitoring
// purposes and that we test on new android versions, this means that any difference in
// inflation will be caught on those devices.
if (LAYOUT_INFLATER_SERVICE == name && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
if (inflater == null) { if (inflater == null) {
inflater = PerformanceInflater(LayoutInflater.from(baseContext), this) inflater = PerformanceInflater(LayoutInflater.from(baseContext), this)
} }
@ -885,7 +943,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
webExtensionId = webExtensionState.id, webExtensionId = webExtensionState.id,
webExtensionTitle = webExtensionState.name webExtensionTitle = webExtensionState.name
) )
navHost.navController.navigate(action) navHost.navController.navigateBlockingForAsyncNavGraph(action)
} }
/** /**

@ -358,7 +358,6 @@ class AddonsManagementFragment : Fragment(R.layout.fragment_add_ons_management)
adapter?.updateAddon(it) adapter?.updateAddon(it)
addonProgressOverlay?.visibility = View.GONE addonProgressOverlay?.visibility = View.GONE
showInstallationDialog(it) showInstallationDialog(it)
Addons.hasInstalledAddons.set(true)
} }
}, },
onError = { _, e -> onError = { _, e ->

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
/** /**
@ -55,6 +56,6 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment( AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
unsupportedAddons.toTypedArray() unsupportedAddons.toTypedArray()
) )
navController.navigate(directions) navController.navigateBlockingForAsyncNavGraph(directions)
} }
} }

@ -21,10 +21,11 @@ import kotlinx.coroutines.launch
import mozilla.components.feature.addons.Addon import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.AddonManagerException import mozilla.components.feature.addons.AddonManagerException
import mozilla.components.feature.addons.ui.translateName import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -128,7 +129,6 @@ class InstalledAddonDetailsFragment : Fragment() {
) )
) )
} }
Addons.hasEnabledAddons.set(true)
} }
}, },
onError = { onError = {
@ -196,6 +196,9 @@ class InstalledAddonDetailsFragment : Fragment() {
view.settings.apply { view.settings.apply {
isVisible = shouldSettingsBeVisible() isVisible = shouldSettingsBeVisible()
setOnClickListener { setOnClickListener {
requireContext().components.analytics.metrics.track(
Event.AddonOpenSetting(addon.id)
)
val settingUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener val settingUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener
val directions = if (addon.installedState?.openOptionsPageInTab == true) { val directions = if (addon.installedState?.openOptionsPageInTab == true) {
val components = it.context.components val components = it.context.components
@ -213,7 +216,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon) .actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
} }
Navigation.findNavController(this).navigate(directions) Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions)
} }
} }
} }
@ -224,7 +227,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment( InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon addon
) )
Navigation.findNavController(view).navigate(directions) Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
} }
} }
@ -234,7 +237,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment( InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
addon addon
) )
Navigation.findNavController(view).navigate(directions) Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
} }
} }

@ -0,0 +1,25 @@
/* 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.android
import android.app.Activity
import android.app.Application
import android.os.Bundle
/**
* An inheritance of [Application.ActivityLifecycleCallbacks] where each method has a default
* implementation that does nothing. This allows classes that extend this interface to have
* more concise definitions if they don't implement some methods; this is in the spirit of
* other `Default*` classes, such as [androidx.lifecycle.DefaultLifecycleObserver].
*/
interface DefaultActivityLifecycleCallbacks : Application.ActivityLifecycleCallbacks {
override fun onActivityCreated(activity: Activity, bundle: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, bundle: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}

@ -80,6 +80,7 @@ import mozilla.components.service.sync.logins.DefaultLoginValidationDelegate
import mozilla.components.support.base.feature.PermissionsFeature import mozilla.components.support.base.feature.PermissionsFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
@ -127,9 +128,9 @@ import java.lang.ref.WeakReference
import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature import mozilla.components.feature.webauthn.WebAuthnFeature
import mozilla.components.support.base.feature.ActivityResultHandler import mozilla.components.support.base.feature.ActivityResultHandler
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import mozilla.components.support.ktx.android.view.enterToImmersiveMode
import org.mozilla.fenix.GleanMetrics.PerfStartup import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.exitImmersiveModeIfNeeded
import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.measureNoInline
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
@ -282,7 +283,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val readerMenuController = DefaultReaderModeController( val readerMenuController = DefaultReaderModeController(
readerViewFeature, readerViewFeature,
view.readerViewControlsBar, view.readerViewControlsBar,
isPrivate = activity.browsingModeManager.mode.isPrivate isPrivate = activity.browsingModeManager.mode.isPrivate,
onReaderModeChanged = { activity.finishActionMode() }
) )
val browserToolbarController = DefaultBrowserToolbarController( val browserToolbarController = DefaultBrowserToolbarController(
store = store, store = store,
@ -370,8 +372,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
store = store, store = store,
sessionId = customTabSessionId, sessionId = customTabSessionId,
stub = view.stubFindInPage, stub = view.stubFindInPage,
engineView = view.engineView, engineView = engineView,
toolbar = browserToolbarView.view toolbarInfo = FindInPageIntegration.ToolbarInfo(
browserToolbarView.view,
!context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled,
!context.settings().shouldUseBottomToolbar
)
), ),
owner = this, owner = this,
view = view view = view
@ -552,7 +558,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
showPage = true, showPage = true,
sessionId = getCurrentTab()?.id sessionId = getCurrentTab()?.id
) )
findNavController().navigate(directions) findNavController().navigateBlockingForAsyncNavGraph(directions)
} }
}, },
onNeedToRequestPermissions = { permissions -> onNeedToRequestPermissions = { permissions ->
@ -563,7 +569,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserAnimator.captureEngineViewAndDrawStatically { browserAnimator.captureEngineViewAndDrawStatically {
val directions = val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment() NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigate(directions) findNavController().navigateBlockingForAsyncNavGraph(directions)
} }
} }
), ),
@ -851,12 +857,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view: View view: View
): List<ContextMenuCandidate> ): List<ContextMenuCandidate>
@CallSuper
override fun onStart() {
super.onStart()
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
}
@VisibleForTesting @VisibleForTesting
internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) { internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) {
val activity = activity as HomeActivity val activity = activity as HomeActivity
@ -980,7 +980,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
} }
override fun onBackLongPressed(): Boolean { override fun onBackLongPressed(): Boolean {
findNavController().navigate( findNavController().navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalTabHistoryDialogFragment( NavGraphDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId activeSessionId = customTabSessionId
) )

@ -33,6 +33,7 @@ import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
@ -264,7 +265,7 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
) )
.setText(view.context.getString(messageStringRes)) .setText(view.context.getString(messageStringRes))
.setAction(requireContext().getString(R.string.create_collection_view)) { .setAction(requireContext().getString(R.string.create_collection_view)) {
findNavController().navigate( findNavController().navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false) BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = false)
) )
} }

@ -33,8 +33,10 @@ open class InfoBanner(
private val dismissText: String, private val dismissText: String,
private val actionText: String? = null, private val actionText: String? = null,
private val dismissByHiding: Boolean = false, private val dismissByHiding: Boolean = false,
private val dismissAction: (() -> Unit)? = null, @VisibleForTesting
private val actionToPerform: (() -> Unit)? = null internal val dismissAction: (() -> Unit)? = null,
@VisibleForTesting
internal val actionToPerform: (() -> Unit)? = null
) { ) {
@SuppressLint("InflateParams") @SuppressLint("InflateParams")
@VisibleForTesting @VisibleForTesting

@ -24,9 +24,11 @@ interface ReaderModeController {
class DefaultReaderModeController( class DefaultReaderModeController(
private val readerViewFeature: ViewBoundFeatureWrapper<ReaderViewFeature>, private val readerViewFeature: ViewBoundFeatureWrapper<ReaderViewFeature>,
private val readerViewControlsBar: View, private val readerViewControlsBar: View,
private val isPrivate: Boolean = false private val isPrivate: Boolean = false,
private val onReaderModeChanged: () -> Unit = {}
) : ReaderModeController { ) : ReaderModeController {
override fun hideReaderView() { override fun hideReaderView() {
onReaderModeChanged()
readerViewFeature.withFeature { readerViewFeature.withFeature {
it.hideReaderView() it.hideReaderView()
it.hideControls() it.hideControls()
@ -34,6 +36,7 @@ class DefaultReaderModeController(
} }
override fun showReaderView() { override fun showReaderView() {
onReaderModeChanged()
readerViewFeature.withFeature { it.showReaderView() } readerViewFeature.withFeature { it.showReaderView() }
} }

@ -0,0 +1,135 @@
/* 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.collections
import android.content.Context
import android.view.LayoutInflater
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.R
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.ext.getDefaultCollectionNumber
/**
* A lambda that is invoked when a confirmation button in a [CollectionsDialog] is clicked.
*
* A [TabCollection] of the selected collected is passed to the delegate when confirmed. If null,
* then a new collection is created.
*
* A list of [TabSessionState] is returned that will be put into the collections storage.
*/
typealias OnPositiveButtonClick = (collection: TabCollection?) -> List<TabSessionState>
/**
* A lambda that is invoked when a cancel button in a [CollectionsDialog] is clicked.
*/
typealias OnNegativeButtonClick = () -> Unit
/**
* A data class for creating a dialog to prompt adding/creating a collection. See also [show].
*
* @property onPositiveButtonClick Invoked when a user clicks on a confirmation button in the dialog.
* @property onNegativeButtonClick Invoked when a user clicks on a cancel button in the dialog.
*/
data class CollectionsDialog(
val storage: TabCollectionStorage,
val onPositiveButtonClick: OnPositiveButtonClick,
val onNegativeButtonClick: OnNegativeButtonClick
)
/**
* Create and display a [CollectionsDialog] using [AlertDialog].
*/
fun CollectionsDialog.show(
context: Context
) {
if (storage.cachedTabCollections.isEmpty()) {
showAddNewDialog(context, storage)
return
}
val collections = storage.cachedTabCollections.map { it.title }
val layout = LayoutInflater.from(context).inflate(R.layout.add_new_collection_dialog, null)
val list = layout.findViewById<RecyclerView>(R.id.recycler_view)
val builder = AlertDialog.Builder(context).setTitle(R.string.tab_tray_select_collection)
.setView(layout)
.setPositiveButton(android.R.string.ok) { dialog, _ ->
val selectedCollection =
(list.adapter as CollectionsListAdapter).getSelectedCollection()
val collection = storage.cachedTabCollections[selectedCollection]
val sessionList = onPositiveButtonClick.invoke(collection)
MainScope().launch {
storage.addTabsToCollection(collection, sessionList)
}
dialog.dismiss()
}.setNegativeButton(android.R.string.cancel) { dialog, _ ->
onNegativeButtonClick.invoke()
dialog.cancel()
}
val dialog = builder.create()
val collectionNames =
arrayOf(context.getString(R.string.tab_tray_add_new_collection)) + collections
val collectionsListAdapter = CollectionsListAdapter(collectionNames) {
dialog.dismiss()
showAddNewDialog(context, storage)
}
list.apply {
layoutManager = LinearLayoutManager(context)
adapter = collectionsListAdapter
}
dialog.show()
}
internal fun CollectionsDialog.showAddNewDialog(
context: Context,
collectionsStorage: TabCollectionStorage
) {
val layout = LayoutInflater.from(context).inflate(R.layout.name_collection_dialog, null)
val collectionNameEditText: EditText = layout.findViewById(R.id.collection_name)
collectionNameEditText.setText(
context.getString(
R.string.create_collection_default_name,
collectionsStorage.cachedTabCollections.getDefaultCollectionNumber()
)
)
AlertDialog.Builder(context)
.setTitle(R.string.tab_tray_add_new_collection)
.setView(layout).setPositiveButton(android.R.string.ok) { dialog, _ ->
val sessionList = onPositiveButtonClick.invoke(null)
MainScope().launch {
storage.createCollection(
collectionNameEditText.text.toString(),
sessionList
)
}
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel) { dialog, _ ->
onNegativeButtonClick.invoke()
dialog.cancel()
}
.create()
.show()
collectionNameEditText.setSelection(0, collectionNameEditText.text.length)
collectionNameEditText.showKeyboard()
}

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.tabtray package org.mozilla.fenix.collections
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@ -14,10 +14,14 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
import org.mozilla.fenix.R import org.mozilla.fenix.R
internal class CollectionsAdapter( /**
* An adapter for displaying an option to create a new collection and the list of existing
* collections.
*/
class CollectionsListAdapter(
private val collections: Array<String>, private val collections: Array<String>,
private val onNewCollectionClicked: () -> Unit private val onNewCollectionClicked: () -> Unit
) : RecyclerView.Adapter<CollectionsAdapter.CollectionItemViewHolder>() { ) : RecyclerView.Adapter<CollectionsListAdapter.CollectionItemViewHolder>() {
@VisibleForTesting @VisibleForTesting
internal var checkedPosition = 1 internal var checkedPosition = 1

@ -0,0 +1,43 @@
/* 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
import androidx.annotation.CallSuper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
/**
* Helper class for creating small binding classes that are responsible for reacting to state
* changes.
*
* Taken with from Focus.
*/
abstract class AbstractBinding<in S : State>(
private val store: Store<S, out Action>
) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
@OptIn(ExperimentalCoroutinesApi::class)
@CallSuper
override fun start() {
scope = store.flowScoped { flow ->
onState(flow)
}
}
@CallSuper
override fun stop() {
scope?.cancel()
}
abstract suspend fun onState(flow: Flow<S>)
}

@ -22,7 +22,6 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.components.metrics.AdjustMetricsService import org.mozilla.fenix.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.LeanplumMetricsService
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -88,13 +87,10 @@ class Analytics(
) )
} }
val leanplumMetricsService by lazyMonitored { LeanplumMetricsService(context as Application) }
val metrics: MetricController by lazyMonitored { val metrics: MetricController by lazyMonitored {
MetricController.create( MetricController.create(
listOf( listOf(
GleanMetricsService(context, lazy { context.components.core.store }), GleanMetricsService(context, lazy { context.components.core.store }),
leanplumMetricsService,
AdjustMetricsService(context as Application) AdjustMetricsService(context as Application)
), ),
isDataTelemetryEnabled = { context.settings().isTelemetryEnabled }, isDataTelemetryEnabled = { context.settings().isTelemetryEnabled },

@ -216,14 +216,9 @@ internal class TelemetryAccountObserver(
}?.let { }?.let {
metricController.track(it) metricController.track(it)
} }
// Used by Leanplum as a context variable.
settings.fxaSignedIn = true
} }
override fun onLoggedOut() { override fun onLoggedOut() {
metricController.track(Event.SyncAuthSignOut) metricController.track(Event.SyncAuthSignOut)
// Used by Leanplum as a context variable.
settings.fxaSignedIn = false
} }
} }

@ -24,10 +24,13 @@ import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.autofill.AutofillUnlockActivity import org.mozilla.fenix.autofill.AutofillUnlockActivity
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.AppStartReasonProvider
import org.mozilla.fenix.perf.StartupActivityLog
import org.mozilla.fenix.perf.StartupStateProvider
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
@ -68,7 +71,8 @@ class Components(private val context: Context) {
core.sessionManager, core.sessionManager,
core.store, core.store,
core.webAppShortcutManager, core.webAppShortcutManager,
core.topSitesStorage core.topSitesStorage,
core.bookmarksStorage
) )
} }
@ -173,4 +177,8 @@ class Components(private val context: Context) {
httpClient = core.client httpClient = core.client
) )
} }
val appStartReasonProvider by lazyMonitored { AppStartReasonProvider() }
val startupActivityLog by lazyMonitored { StartupActivityLog() }
val startupStateProvider by lazyMonitored { StartupStateProvider(startupActivityLog, appStartReasonProvider) }
} }

@ -35,6 +35,7 @@ import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.MediaSessionFeature import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.prompts.PromptMiddleware
import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware import mozilla.components.feature.readerview.ReaderViewMiddleware
@ -64,7 +65,6 @@ import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.components.search.SearchMigration import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -76,6 +76,7 @@ import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
import org.mozilla.fenix.settings.SupportUtils import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.telemetry.TelemetryMiddleware
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay import org.mozilla.fenix.utils.getUndoDelay
@ -194,7 +195,8 @@ class Core(
additionalBundledSearchEngineIds = listOf("reddit", "youtube"), additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context) migration = SearchMigration(context)
), ),
RecordingDevicesMiddleware(context) RecordingDevicesMiddleware(context),
PromptMiddleware()
) )
BrowserStore( BrowserStore(
@ -335,6 +337,13 @@ class Core(
SupportUtils.JD_URL SupportUtils.JD_URL
) )
) )
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_pdd),
SupportUtils.PDD_URL
)
)
} else { } else {
defaultTopSites.add( defaultTopSites.add(
Pair( Pair(

@ -5,7 +5,10 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.view.View import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.view.ViewStub import android.view.ViewStub
import androidx.annotation.VisibleForTesting
import androidx.core.view.isVisible
import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab import mozilla.components.browser.state.selector.findCustomTabOrSelectedTab
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
@ -13,30 +16,100 @@ import mozilla.components.concept.engine.EngineView
import mozilla.components.feature.findinpage.FindInPageFeature import mozilla.components.feature.findinpage.FindInPageFeature
import mozilla.components.feature.findinpage.view.FindInPageView import mozilla.components.feature.findinpage.view.FindInPageView
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.components.FindInPageIntegration.ToolbarInfo
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
/**
* BrowserFragment delegate to handle all layout updates needed to show or hide the find in page bar.
*
* @param store [BrowserStore]
* @param sessionId ID of the [store] session in which the query will be performed.
* @param engineView the browser in which the queries will be made and which needs to be better positioned
* to suit the find in page bar.
* @param toolbarInfo [ToolbarInfo] used to configure the [BrowserToolbar] while the find in page bar is shown.
*/
@Mockable @Mockable
class FindInPageIntegration( class FindInPageIntegration(
private val store: BrowserStore, private val store: BrowserStore,
private val sessionId: String? = null, private val sessionId: String? = null,
stub: ViewStub, stub: ViewStub,
private val engineView: EngineView, private val engineView: EngineView,
private val toolbar: BrowserToolbar private val toolbarInfo: ToolbarInfo
) : InflationAwareFeature(stub) { ) : InflationAwareFeature(stub) {
override fun onViewInflated(view: View): LifecycleAwareFeature { override fun onViewInflated(view: View): LifecycleAwareFeature {
return FindInPageFeature(store, view as FindInPageView, engineView) { return FindInPageFeature(store, view as FindInPageView, engineView) {
toolbar.visibility = View.VISIBLE restorePreviousLayout()
view.visibility = View.GONE view.visibility = View.GONE
} }
} }
override fun onLaunch(view: View, feature: LifecycleAwareFeature) { override fun onLaunch(view: View, feature: LifecycleAwareFeature) {
store.state.findCustomTabOrSelectedTab(sessionId)?.let { tab -> store.state.findCustomTabOrSelectedTab(sessionId)?.let { tab ->
// Always hide the toolbar and display find in page query prepareLayoutForFindBar()
toolbar.visibility = View.GONE
view.visibility = View.VISIBLE view.visibility = View.VISIBLE
(feature as FindInPageFeature).bind(tab) (feature as FindInPageFeature).bind(tab)
view.layoutParams.height = toolbar.height view.layoutParams.height = toolbarInfo.toolbar.height
} }
} }
@VisibleForTesting
internal fun restorePreviousLayout() {
toolbarInfo.toolbar.isVisible = true
val engineViewParent = getEngineViewParent()
val engineViewParentParams = getEngineViewsParentLayoutParams()
if (toolbarInfo.isToolbarPlacedAtTop) {
if (toolbarInfo.isToolbarDynamic) {
engineViewParent.translationY = toolbarInfo.toolbar.height.toFloat()
engineViewParentParams.bottomMargin = 0
} else {
engineViewParent.translationY = 0f
}
} else {
if (toolbarInfo.isToolbarDynamic) {
engineViewParentParams.bottomMargin = 0
}
}
}
@VisibleForTesting
internal fun prepareLayoutForFindBar() {
toolbarInfo.toolbar.isVisible = false
val engineViewParent = getEngineViewParent()
val engineViewParentParams = getEngineViewsParentLayoutParams()
if (toolbarInfo.isToolbarPlacedAtTop) {
if (toolbarInfo.isToolbarDynamic) {
// With a dynamic toolbar the EngineView extends to the entire (top and bottom) of the screen.
// And now with the toolbar expanded it is translated down immediately below the toolbar.
engineViewParent.translationY = 0f
engineViewParentParams.bottomMargin = toolbarInfo.toolbar.height
} else {
// With a fixed toolbar the EngineView is anchored below the toolbar with 0 Y translation.
engineViewParent.translationY = -toolbarInfo.toolbar.height.toFloat()
}
} else {
// With a bottom toolbar the EngineView is already anchored to the top of the screen.
// Need just to ensure space for the find in page bar under the engineView.
engineViewParentParams.bottomMargin = toolbarInfo.toolbar.height
}
}
@VisibleForTesting
internal fun getEngineViewParent() = engineView.asView().parent as View
@VisibleForTesting
internal fun getEngineViewsParentLayoutParams() = getEngineViewParent().layoutParams as MarginLayoutParams
/**
* Holder of all details needed about the Toolbar.
* Used to modify the layout of BrowserToolbar while the find in page bar is shown.
*/
data class ToolbarInfo(
val toolbar: BrowserToolbar,
val isToolbarDynamic: Boolean,
val isToolbarPlacedAtTop: Boolean
)
} }

@ -6,6 +6,7 @@ package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import mozilla.components.service.fxa.ServerConfig import mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.ServerConfig.Server import mozilla.components.service.fxa.ServerConfig.Server
import org.mozilla.fenix.Config
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
/** /**
@ -17,10 +18,18 @@ object FxaServer {
const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel" const val REDIRECT_URL = "urn:ietf:wg:oauth:2.0:oob:oauth-redirect-webchannel"
fun config(context: Context): ServerConfig { fun config(context: Context): ServerConfig {
// If a server override is configured, use that. Otherwise:
// - for all channels other than Mozilla Online, use Server.Release.
// - for Mozilla Online channel, if domestic server is allowed, use Server.CHINA; otherwise, use Server.RELEASE
val serverOverride = context.settings().overrideFxAServer val serverOverride = context.settings().overrideFxAServer
val tokenServerOverride = context.settings().overrideSyncTokenServer.ifEmpty { null } val tokenServerOverride = context.settings().overrideSyncTokenServer.ifEmpty { null }
if (serverOverride.isEmpty()) { if (serverOverride.isEmpty()) {
return ServerConfig(Server.RELEASE, CLIENT_ID, REDIRECT_URL, tokenServerOverride) val releaseServer = if (Config.channel.isMozillaOnline && context.settings().allowDomesticChinaFxaServer) {
Server.CHINA
} else {
Server.RELEASE
}
return ServerConfig(releaseServer, CLIENT_ID, REDIRECT_URL, tokenServerOverride)
} }
return ServerConfig(serverOverride, CLIENT_ID, REDIRECT_URL, tokenServerOverride) return ServerConfig(serverOverride, CLIENT_ID, REDIRECT_URL, tokenServerOverride)
} }

@ -5,6 +5,7 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.ColdStartupDurationTelemetry
import org.mozilla.fenix.perf.VisualCompletenessQueue import org.mozilla.fenix.perf.VisualCompletenessQueue
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
@ -13,4 +14,5 @@ import org.mozilla.fenix.perf.lazyMonitored
*/ */
class PerformanceComponent { class PerformanceComponent {
val visualCompletenessQueue by lazyMonitored { VisualCompletenessQueue(RunWhenReadyQueue()) } val visualCompletenessQueue by lazyMonitored { VisualCompletenessQueue(RunWhenReadyQueue()) }
val coldStartupDurationTelemetry by lazyMonitored { ColdStartupDurationTelemetry() }
} }

@ -8,6 +8,7 @@ import android.content.Context
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuUseCases import mozilla.components.feature.contextmenu.ContextMenuUseCases
import mozilla.components.feature.downloads.DownloadsUseCases import mozilla.components.feature.downloads.DownloadsUseCases
@ -23,6 +24,7 @@ import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.feature.top.sites.TopSitesUseCases
import mozilla.components.support.locale.LocaleUseCases import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
@ -38,7 +40,8 @@ class UseCases(
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
private val shortcutManager: WebAppShortcutManager, private val shortcutManager: WebAppShortcutManager,
private val topSitesStorage: TopSitesStorage private val topSitesStorage: TopSitesStorage,
private val bookmarksStorage: BookmarksStorage
) { ) {
/** /**
* Use cases that provide engine interactions for a given browser session. * Use cases that provide engine interactions for a given browser session.
@ -94,4 +97,9 @@ class UseCases(
* Use cases that handle locale management. * Use cases that handle locale management.
*/ */
val localeUseCases by lazyMonitored { LocaleUseCases(store) } val localeUseCases by lazyMonitored { LocaleUseCases(store) }
/**
* Use cases that provide bookmark management.
*/
val bookmarksUseCases by lazyMonitored { BookmarksUseCase(bookmarksStorage) }
} }

@ -0,0 +1,33 @@
/* 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.accounts
import android.content.Context
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.ext.components
/**
* Component which holds a reference to [FxaAccountManager]. Manages account authentication,
* profiles, and profile state observers.
*/
open class FenixAccountManager(context: Context) {
val accountManager = context.components.backgroundServices.accountManager
val authenticatedAccount
get() = accountManager.authenticatedAccount() != null
val accountProfileEmail
get() = accountManager.accountProfile()?.email
/**
* Check if the current account is signed in and authenticated.
*/
fun signedInToFxa(): Boolean {
val account = accountManager.authenticatedAccount()
val needsReauth = accountManager.accountNeedsReauth()
return account != null && !needsReauth
}
}

@ -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.components.bookmarks
import androidx.annotation.WorkerThread
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.concept.storage.BookmarksStorage
/**
* Use cases that allow for modifying bookmarks.
*/
class BookmarksUseCase(storage: BookmarksStorage) {
class AddBookmarksUseCase internal constructor(private val storage: BookmarksStorage) {
/**
* Adds a new bookmark with the provided [url] and [title].
*
* @return The result if the operation was executed or not. A bookmark may not be added if
* one with the identical [url] already exists.
*/
@WorkerThread
suspend operator fun invoke(url: String, title: String, position: Int? = null): Boolean {
val canAdd = storage.getBookmarksWithUrl(url).firstOrNull { it.url == it.url } == null
if (canAdd) {
storage.addItem(
BookmarkRoot.Mobile.id,
url = url,
title = title,
position = position
)
}
return canAdd
}
}
val addBookmark by lazy { AddBookmarksUseCase(storage) }
}

@ -33,6 +33,12 @@ import java.lang.reflect.Modifier.PRIVATE
* Sample = [source = COLD, type = APP_ICON, hasSavedInstanceState = false,launchTimeNanoSeconds = 1824000000] * Sample = [source = COLD, type = APP_ICON, hasSavedInstanceState = false,launchTimeNanoSeconds = 1824000000]
* The basic idea is to collect these metrics from different phases of startup through * The basic idea is to collect these metrics from different phases of startup through
* [AppAllStartup] and finally report them on Activity's onResume() function. * [AppAllStartup] and finally report them on Activity's onResume() function.
*
* **THIS CLASS HAS A KNOWN FLAW:** for COLD start, it doesn't take into account if the process is
* already running when the app starts, possibly inflating results (e.g. a Service started the
* process 20min ago and only now is HomeActivity launching). Future telemetry implementations should
* probably move in the ideological direction of [org.mozilla.fenix.perf.ColdStartupDurationTelemetry]:
* simplicity rather than comprehensiveness.
*/ */
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class AppStartupTelemetry( class AppStartupTelemetry(

@ -182,6 +182,7 @@ sealed class Event {
object ClosedExistingTab : Event() object ClosedExistingTab : Event()
object TabsTrayPrivateModeTapped : Event() object TabsTrayPrivateModeTapped : Event()
object TabsTrayNormalModeTapped : Event() object TabsTrayNormalModeTapped : Event()
object TabsTraySyncedModeTapped : Event()
object NewTabTapped : Event() object NewTabTapped : Event()
object NewPrivateTabTapped : Event() object NewPrivateTabTapped : Event()
object TabsTrayMenuOpened : Event() object TabsTrayMenuOpened : Event()
@ -224,6 +225,17 @@ sealed class Event {
object SearchSuggestionClicked : Event() object SearchSuggestionClicked : Event()
object OpenedTabSuggestionClicked : Event() object OpenedTabSuggestionClicked : Event()
// Set default browser experiment metrics
object SetDefaultBrowserNewTabClicked : Event()
object CloseExperimentCardClicked : Event()
object ToolbarMenuShown : Event()
object SetDefaultBrowserToolbarMenuClicked : Event()
object SetDefaultBrowserSettingsScreenClicked : Event()
// Home menu interaction
object HomeMenuSettingsItemClicked : Event()
object HomeScreenDisplayed : Event()
// Interaction events with extras // Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() { data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -325,6 +337,11 @@ sealed class Event {
get() = hashMapOf(Addons.openAddonInToolbarMenuKeys.addonId to addonId) get() = hashMapOf(Addons.openAddonInToolbarMenuKeys.addonId to addonId)
} }
data class AddonOpenSetting(val addonId: String) : Event() {
override val extras: Map<Addons.openAddonSettingKeys, String>?
get() = hashMapOf(Addons.openAddonSettingKeys.addonId to addonId)
}
data class TipDisplayed(val identifier: String) : Event() { data class TipDisplayed(val identifier: String) : Event() {
override val extras: Map<Tip.displayedKeys, String>? override val extras: Map<Tip.displayedKeys, String>?
get() = hashMapOf(Tip.displayedKeys.identifier to identifier) get() = hashMapOf(Tip.displayedKeys.identifier to identifier)
@ -577,7 +594,7 @@ sealed class Event {
NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX, NEW_PRIVATE_TAB, SHARE, BACK, FORWARD, RELOAD, STOP, OPEN_IN_FENIX,
SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON, SAVE_TO_COLLECTION, ADD_TO_TOP_SITES, ADD_TO_HOMESCREEN, QUIT, READER_MODE_ON,
READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER, READER_MODE_OFF, OPEN_IN_APP, BOOKMARK, READER_MODE_APPEARANCE, ADDONS_MANAGER,
BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS, SET_DEFAULT_BROWSER, SYNC_ACCOUNT
} }
override val extras: Map<Events.browserMenuActionKeys, String>? override val extras: Map<Events.browserMenuActionKeys, String>?
@ -597,7 +614,7 @@ sealed class Event {
data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() { data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() {
enum class AutoplaySetting { enum class AutoplaySetting {
BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL, ALLOW_ALL
} }
override val extras: Map<Autoplay.settingChangedKeys, String>? override val extras: Map<Autoplay.settingChangedKeys, String>?

@ -32,8 +32,11 @@ import org.mozilla.fenix.GleanMetrics.DownloadsMisc
import org.mozilla.fenix.GleanMetrics.DownloadsManagement import org.mozilla.fenix.GleanMetrics.DownloadsManagement
import org.mozilla.fenix.GleanMetrics.ErrorPage import org.mozilla.fenix.GleanMetrics.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.ExperimentsDefaultBrowser
import org.mozilla.fenix.GleanMetrics.FindInPage import org.mozilla.fenix.GleanMetrics.FindInPage
import org.mozilla.fenix.GleanMetrics.History import org.mozilla.fenix.GleanMetrics.History
import org.mozilla.fenix.GleanMetrics.HomeMenu
import org.mozilla.fenix.GleanMetrics.HomeScreen
import org.mozilla.fenix.GleanMetrics.LoginDialog import org.mozilla.fenix.GleanMetrics.LoginDialog
import org.mozilla.fenix.GleanMetrics.Logins import org.mozilla.fenix.GleanMetrics.Logins
import org.mozilla.fenix.GleanMetrics.MasterPassword import org.mozilla.fenix.GleanMetrics.MasterPassword
@ -52,6 +55,8 @@ import org.mozilla.fenix.GleanMetrics.SearchDefaultEngine
import org.mozilla.fenix.GleanMetrics.SearchShortcuts import org.mozilla.fenix.GleanMetrics.SearchShortcuts
import org.mozilla.fenix.GleanMetrics.SearchSuggestions import org.mozilla.fenix.GleanMetrics.SearchSuggestions
import org.mozilla.fenix.GleanMetrics.SearchWidget import org.mozilla.fenix.GleanMetrics.SearchWidget
import org.mozilla.fenix.GleanMetrics.SetDefaultNewtabExperiment
import org.mozilla.fenix.GleanMetrics.SetDefaultSettingExperiment
import org.mozilla.fenix.GleanMetrics.SyncAccount import org.mozilla.fenix.GleanMetrics.SyncAccount
import org.mozilla.fenix.GleanMetrics.SyncAuth import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.SyncedTabs import org.mozilla.fenix.GleanMetrics.SyncedTabs
@ -192,6 +197,15 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.browserMenuAction.record(it) }, { Events.browserMenuAction.record(it) },
{ Events.browserMenuActionKeys.valueOf(it) } { Events.browserMenuActionKeys.valueOf(it) }
) )
is Event.SetDefaultBrowserToolbarMenuClicked -> EventWrapper<NoExtraKeys>(
{ ExperimentsDefaultBrowser.toolbarMenuClicked.record(it) }
)
is Event.ToolbarMenuShown -> EventWrapper<NoExtraKeys>(
{ Events.toolbarMenuVisible.record(it) }
)
is Event.ChangedToDefaultBrowser -> EventWrapper<NoExtraKeys>(
{ Events.defaultBrowserChanged.record(it) }
)
is Event.OpenedBookmark -> EventWrapper<NoExtraKeys>( is Event.OpenedBookmark -> EventWrapper<NoExtraKeys>(
{ BookmarksManagement.open.record(it) } { BookmarksManagement.open.record(it) }
) )
@ -586,6 +600,10 @@ private val Event.wrapper: EventWrapper<*>?
{ Addons.openAddonInToolbarMenu.record(it) }, { Addons.openAddonInToolbarMenu.record(it) },
{ Addons.openAddonInToolbarMenuKeys.valueOf(it) } { Addons.openAddonInToolbarMenuKeys.valueOf(it) }
) )
is Event.AddonOpenSetting -> EventWrapper(
{ Addons.openAddonSetting.record(it) },
{ Addons.openAddonSettingKeys.valueOf(it) }
)
is Event.TipDisplayed -> EventWrapper( is Event.TipDisplayed -> EventWrapper(
{ Tip.displayed.record(it) }, { Tip.displayed.record(it) },
{ Tip.displayedKeys.valueOf(it) } { Tip.displayedKeys.valueOf(it) }
@ -667,6 +685,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>( is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.normalModeTapped.record(it) } { TabsTray.normalModeTapped.record(it) }
) )
is Event.TabsTraySyncedModeTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.syncedModeTapped.record(it) }
)
is Event.NewTabTapped -> EventWrapper<NoExtraKeys>( is Event.NewTabTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.newTabTapped.record(it) } { TabsTray.newTabTapped.record(it) }
) )
@ -805,6 +826,22 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SecurePrefsReset -> EventWrapper<NoExtraKeys>( is Event.SecurePrefsReset -> EventWrapper<NoExtraKeys>(
{ AndroidKeystoreExperiment.reset.record(it) } { AndroidKeystoreExperiment.reset.record(it) }
) )
is Event.HomeMenuSettingsItemClicked -> EventWrapper<NoExtraKeys>(
{ HomeMenu.settingsItemClicked.record(it) }
)
is Event.CloseExperimentCardClicked -> EventWrapper<NoExtraKeys>(
{ SetDefaultNewtabExperiment.closeExperimentCardClicked.record(it) }
)
is Event.SetDefaultBrowserNewTabClicked -> EventWrapper<NoExtraKeys>(
{ SetDefaultNewtabExperiment.setDefaultBrowserClicked.record(it) }
)
is Event.SetDefaultBrowserSettingsScreenClicked -> EventWrapper<NoExtraKeys>(
{ SetDefaultSettingExperiment.setDefaultBrowserClicked.record(it) }
)
is Event.HomeScreenDisplayed -> EventWrapper<NoExtraKeys>(
{ HomeScreen.homeScreenDisplayed.record(it) }
)
// Don't record other events in Glean: // Don't record other events in Glean:
is Event.AddBookmark -> null is Event.AddBookmark -> null
@ -815,7 +852,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.FennecToFenixMigrated -> null is Event.FennecToFenixMigrated -> null
is Event.AddonInstalled -> null is Event.AddonInstalled -> null
is Event.SearchWidgetInstalled -> null is Event.SearchWidgetInstalled -> null
is Event.ChangedToDefaultBrowser -> null
is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> null is Event.SyncAuthFromSharedReuse, Event.SyncAuthFromSharedCopy -> null
} }

@ -1,247 +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.metrics
import android.app.Application
import android.net.Uri
import android.util.Log
import com.leanplum.Leanplum
import com.leanplum.LeanplumActivityHelper
import com.leanplum.annotations.Parser
import com.leanplum.internal.LeanplumInternal
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.components.metrics.MozillaProductDetector.MozillaProducts
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
import java.util.Locale
import java.util.MissingResourceException
import java.util.UUID.randomUUID
private val Event.name: String?
get() = when (this) {
is Event.AddBookmark -> "E_Add_Bookmark"
is Event.RemoveBookmark -> "E_Remove_Bookmark"
is Event.OpenedBookmark -> "E_Opened_Bookmark"
is Event.OpenedApp -> "E_Opened_App"
is Event.OpenedAppFirstRun -> "E_Opened_App_FirstRun"
is Event.InteractWithSearchURLArea -> "E_Interact_With_Search_URL_Area"
is Event.CollectionSaved -> "E_Collection_Created"
is Event.CollectionTabRestored -> "E_Collection_Tab_Opened"
is Event.SyncAuthSignUp -> "E_FxA_New_Signup"
is Event.SyncAuthSignIn, Event.SyncAuthPaired, Event.SyncAuthOtherExternal -> "E_Sign_In_FxA"
is Event.SyncAuthFromSharedCopy, Event.SyncAuthFromSharedReuse -> "E_Sign_In_FxA_Fennec_to_Fenix"
is Event.SyncAuthSignOut -> "E_Sign_Out_FxA"
is Event.ClearedPrivateData -> "E_Cleared_Private_Data"
is Event.DismissedOnboarding -> "E_Dismissed_Onboarding"
is Event.FennecToFenixMigrated -> "E_Fennec_To_Fenix_Migrated"
is Event.AddonInstalled -> "E_Addon_Installed"
is Event.SearchWidgetInstalled -> "E_Search_Widget_Added"
is Event.ChangedToDefaultBrowser -> "E_Changed_Default_To_Fenix"
is Event.TrackingProtectionSettingChanged -> "E_Changed_ETP"
// Do not track other events in Leanplum
else -> null
}
class LeanplumMetricsService(
private val application: Application
) : MetricsService, DeepLinkIntentProcessor.DeepLinkVerifier {
val scope = CoroutineScope(Dispatchers.IO)
var leanplumJob: Job? = null
data class Token(val id: String, val token: String) {
enum class Type { Development, Production, Invalid }
val type by lazy {
when {
token.take(ProdPrefix.length) == ProdPrefix -> Type.Production
token.take(DevPrefix.length) == DevPrefix -> Type.Development
else -> Type.Invalid
}
}
companion object {
private const val ProdPrefix = "prod"
private const val DevPrefix = "dev"
}
}
override val type = MetricServiceType.Marketing
private val token = Token(LeanplumId, LeanplumToken)
@Suppress("ComplexMethod")
override fun start() {
if (!application.settings().isMarketingTelemetryEnabled) return
Leanplum.setIsTestModeEnabled(false)
Leanplum.setApplicationContext(application)
Leanplum.setDeviceId(randomUUID().toString())
Parser.parseVariables(application)
leanplumJob = scope.launch {
val applicationSetLocale = LocaleManager.getCurrentLocale(application)
val currentLocale = applicationSetLocale ?: Locale.getDefault()
val languageCode =
currentLocale.iso3LanguageOrNull
?: currentLocale.language.let {
if (it.isNotBlank()) {
it
} else {
currentLocale.toString()
}
}
if (!isLeanplumEnabled(languageCode)) {
Log.i(LOGTAG, "Leanplum is not available for this locale: $languageCode")
return@launch
}
when (token.type) {
Token.Type.Production -> Leanplum.setAppIdForProductionMode(token.id, token.token)
Token.Type.Development -> Leanplum.setAppIdForDevelopmentMode(token.id, token.token)
Token.Type.Invalid -> {
Log.i(LOGTAG, "Invalid or missing Leanplum token")
return@launch
}
}
LeanplumActivityHelper.enableLifecycleCallbacks(application)
val installedApps = MozillaProductDetector.getInstalledMozillaProducts(application)
val trackingProtection = application.settings().run {
when {
!shouldUseTrackingProtection -> "none"
useStandardTrackingProtection -> "standard"
useStrictTrackingProtection -> "strict"
else -> "custom"
}
}
Leanplum.start(
application, hashMapOf(
"default_browser" to MozillaProductDetector.getMozillaBrowserDefault(application)
.orEmpty(),
"fennec_installed" to installedApps.contains(MozillaProducts.FIREFOX.productName),
"focus_installed" to installedApps.contains(MozillaProducts.FOCUS.productName),
"klar_installed" to installedApps.contains(MozillaProducts.KLAR.productName),
"fxa_signed_in" to application.settings().fxaSignedIn,
"fxa_has_synced_items" to application.settings().fxaHasSyncedItems,
"search_widget_installed" to application.settings().searchWidgetInstalled,
"tracking_protection_enabled" to application.settings().shouldUseTrackingProtection,
"tracking_protection_setting" to trackingProtection,
"fenix" to true
)
)
withContext(Main) {
LeanplumInternal.setCalledStart(true)
LeanplumInternal.setHasStarted(true)
LeanplumInternal.setStartedInBackground(true)
Log.i(LOGTAG, "Started Leanplum with deviceId ${Leanplum.getDeviceId()}" +
" and userId ${Leanplum.getUserId()}")
}
}
}
/**
* Verifies a deep link and returns `true` for deep links that should be handled and `false` if
* a deep link should be rejected.
*
* @See DeepLinkIntentProcessor.verifier
*/
override fun verifyDeepLink(deepLink: Uri): Boolean {
// We compare the local Leanplum device ID against the "uid" query parameter and only
// accept deep links where both values match.
val uid = deepLink.getQueryParameter("uid")
return uid == Leanplum.getDeviceId()
}
override fun stop() {
if (application.settings().isMarketingTelemetryEnabled) return
// As written in LeanPlum SDK documentation, "This prevents Leanplum from communicating with the server."
// as this "isTestMode" flag is checked before LeanPlum SDK does anything.
// Also has the benefit effect of blocking the display of already downloaded messages.
// The reverse of this - setIsTestModeEnabled(false) must be called before trying to init
// LP in the same session.
Leanplum.setIsTestModeEnabled(true)
// This is just to allow restarting LP and it's functionality in the same app session
// as LP stores it's state internally and check against it
LeanplumInternal.setCalledStart(false)
LeanplumInternal.setHasStarted(false)
leanplumJob?.cancel()
}
override fun track(event: Event) {
val leanplumExtras = event.extras
?.map { (key, value) -> key.toString() to value }
?.toMap()
event.name?.also {
Leanplum.track(it, leanplumExtras)
}
}
override fun shouldTrack(event: Event): Boolean {
return application.settings().isTelemetryEnabled &&
token.type != Token.Type.Invalid && !event.name.isNullOrEmpty()
}
private fun isLeanplumEnabled(locale: String): Boolean {
return LEANPLUM_ENABLED_LOCALES.contains(locale)
}
private val Locale.iso3LanguageOrNull: String?
get() =
try {
this.isO3Language
} catch (_: MissingResourceException) {
null
}
companion object {
private const val LOGTAG = "LeanplumMetricsService"
private val LeanplumId: String
// Debug builds have a null (nullable) LEANPLUM_ID
get() = BuildConfig.LEANPLUM_ID.orEmpty()
private val LeanplumToken: String
// Debug builds have a null (nullable) LEANPLUM_TOKEN
get() = BuildConfig.LEANPLUM_TOKEN.orEmpty()
// Leanplum needs to be enabled for the following locales.
// Irrespective of the actual device location.
private val LEANPLUM_ENABLED_LOCALES = setOf(
"eng", // English
"zho", // Chinese
"deu", // German
"fra", // French
"ita", // Italian
"ind", // Indonesian
"por", // Portuguese
"spa", // Spanish; Castilian
"pol", // Polish
"rus", // Russian
"hin", // Hindi
"per", // Persian
"fas", // Persian
"ara", // Arabic
"jpn" // Japanese
)
private const val PREFERENCE_NAME = "LEANPLUM_PREFERENCES"
private const val DEVICE_ID_KEY = "LP_DEVICE_ID"
}
}

@ -5,7 +5,6 @@
package org.mozilla.fenix.components.metrics package org.mozilla.fenix.components.metrics
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts import mozilla.components.browser.toolbar.facts.ToolbarFacts
@ -179,7 +178,7 @@ internal class ReleaseMetricController(
} }
Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> {
metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } ?: Event.ToolbarMenuShown
} }
Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> { Component.BROWSER_MENU to BrowserMenuFacts.Items.WEB_EXTENSION_MENU_ITEM -> {
metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) } metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) }
@ -218,7 +217,6 @@ internal class ReleaseMetricController(
if (installedAddons is List<*>) { if (installedAddons is List<*>) {
settings.installedAddonsCount = installedAddons.size settings.installedAddonsCount = installedAddons.size
settings.installedAddonsList = installedAddons.joinToString(",") settings.installedAddonsList = installedAddons.joinToString(",")
Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size))
} }
} }
@ -226,7 +224,6 @@ internal class ReleaseMetricController(
if (enabledAddons is List<*>) { if (enabledAddons is List<*>) {
settings.enabledAddonsCount = enabledAddons.size settings.enabledAddonsCount = enabledAddons.size
settings.enabledAddonsList = enabledAddons.joinToString() settings.enabledAddonsList = enabledAddons.joinToString()
Leanplum.setUserAttributes(mapOf("enabled_addons" to enabledAddons.size))
} }
} }

@ -24,6 +24,7 @@ import org.mozilla.fenix.browser.readermode.ReaderModeController
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeScreenViewModel import org.mozilla.fenix.home.HomeScreenViewModel
@ -118,7 +119,7 @@ class DefaultBrowserToolbarController(
// When closing the last tab we must show the undo snackbar in the home fragment // When closing the last tab we must show the undo snackbar in the home fragment
if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) { if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) {
homeViewModel.sessionToDelete = it.id homeViewModel.sessionToDelete = it.id
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome() BrowserFragmentDirections.actionGlobalHome()
) )
} else { } else {
@ -132,7 +133,7 @@ class DefaultBrowserToolbarController(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB) Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB)
) )
activity.browsingModeManager.mode = BrowsingMode.Normal activity.browsingModeManager.mode = BrowsingMode.Normal
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
) )
} }
@ -143,7 +144,7 @@ class DefaultBrowserToolbarController(
) )
) )
activity.browsingModeManager.mode = BrowsingMode.Private activity.browsingModeManager.mode = BrowsingMode.Private
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
) )
} }

@ -39,8 +39,10 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Do
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -91,7 +93,7 @@ class DefaultBrowserToolbarMenuController(
Do exhaustive when (item) { Do exhaustive when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870 // TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start === // todo === Start ===
is ToolbarMenu.Item.InstallToHomeScreen -> { is ToolbarMenu.Item.InstallPwaToHomeScreen -> {
settings.installPwaOpened = true settings.installPwaOpened = true
MainScope().launch { MainScope().launch {
with(activity.components.useCases.webAppUseCases) { with(activity.components.useCases.webAppUseCases) {
@ -128,6 +130,18 @@ class DefaultBrowserToolbarMenuController(
activity.finishAndRemoveTask() activity.finishAndRemoveTask()
} }
} }
// todo === End ===
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)
}
}
is ToolbarMenu.Item.Quit -> { is ToolbarMenu.Item.Quit -> {
// We need to show the snackbar while the browsing data is deleting (if "Delete // 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 // browsing data on quit" is activated). After the deletion is over, the snackbar
@ -147,22 +161,9 @@ class DefaultBrowserToolbarMenuController(
readerModeController.showControls() readerModeController.showControls()
metrics.track(Event.ReaderModeAppearanceOpened) metrics.track(Event.ReaderModeAppearanceOpened)
} }
is ToolbarMenu.Item.OpenInApp -> {
settings.openInAppOpened = true
val appLinksUseCases = activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
currentSession?.let {
val redirect = getRedirect.invoke(it.content.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
}
}
// todo === End ===
is ToolbarMenu.Item.Back -> { is ToolbarMenu.Item.Back -> {
if (item.viewHistory) { if (item.viewHistory) {
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId activeSessionId = customTabSessionId
) )
@ -175,7 +176,7 @@ class DefaultBrowserToolbarMenuController(
} }
is ToolbarMenu.Item.Forward -> { is ToolbarMenu.Item.Forward -> {
if (item.viewHistory) { if (item.viewHistory) {
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment( BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId activeSessionId = customTabSessionId
) )
@ -212,7 +213,7 @@ class DefaultBrowserToolbarMenuController(
), ),
showPage = true showPage = true
) )
navController.navigate(directions) navController.navigateBlockingForAsyncNavGraph(directions)
} }
is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically { is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment() val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
@ -224,6 +225,19 @@ class DefaultBrowserToolbarMenuController(
BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment() BrowserFragmentDirections.actionBrowserFragmentToSyncedTabsFragment()
) )
} }
is ToolbarMenu.Item.SyncAccount -> {
val directions = if (item.signedIn) {
BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
} else {
BrowserFragmentDirections.actionGlobalTurnOnSync()
}
browserAnimator.captureEngineViewAndDrawStatically {
navController.nav(
R.id.browserFragment,
directions
)
}
}
is ToolbarMenu.Item.RequestDesktop -> { is ToolbarMenu.Item.RequestDesktop -> {
currentSession?.let { currentSession?.let {
sessionUseCases.requestDesktopSite.invoke( sessionUseCases.requestDesktopSite.invoke(
@ -335,10 +349,14 @@ class DefaultBrowserToolbarMenuController(
) )
} }
is ToolbarMenu.Item.NewTab -> { is ToolbarMenu.Item.NewTab -> {
navController.navigate( navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true) BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
) )
} }
is ToolbarMenu.Item.SetDefaultBrowser -> {
metrics.track(Event.SetDefaultBrowserToolbarMenuClicked)
activity.openSetDefaultBrowserOption()
}
} }
} }
@ -356,15 +374,12 @@ class DefaultBrowserToolbarMenuController(
@Suppress("ComplexMethod") @Suppress("ComplexMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) { private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
val eventItem = when (item) { val eventItem = when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.InstallPwaToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
is ToolbarMenu.Item.CustomizeReaderView -> is ToolbarMenu.Item.CustomizeReaderView ->
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
// todo === End ===
is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
@ -382,12 +397,14 @@ class DefaultBrowserToolbarMenuController(
is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES
is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS
is ToolbarMenu.Item.SyncAccount -> Event.BrowserMenuItemTapped.Item.SYNC_ACCOUNT
is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB
is ToolbarMenu.Item.SetDefaultBrowser -> Event.BrowserMenuItemTapped.Item.SET_DEFAULT_BROWSER
} }
metrics.track(Event.BrowserMenuItemTapped(eventItem)) metrics.track(Event.BrowserMenuItemTapped(eventItem))

@ -168,14 +168,13 @@ class BrowserToolbarView(
} else { } else {
menuToolbar = DefaultToolbarMenu( menuToolbar = DefaultToolbarMenu(
context = this, context = this,
store = components.core.store,
hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(), hasAccountProblem = components.backgroundServices.accountManager.accountNeedsReauth(),
shouldReverseItems = toolbarPosition == ToolbarPosition.TOP,
onItemTapped = { onItemTapped = {
it.performHapticIfNeeded(view) it.performHapticIfNeeded(view)
interactor.onBrowserToolbarMenuItemTapped(it) interactor.onBrowserToolbarMenuItemTapped(it)
}, },
lifecycleOwner = lifecycleOwner, lifecycleOwner = lifecycleOwner,
store = components.core.store,
bookmarksStorage = bookmarkStorage, bookmarksStorage = bookmarkStorage,
isPinningSupported = isPinningSupported isPinningSupported = isPinningSupported
) )

@ -7,6 +7,7 @@ package org.mozilla.fenix.components.toolbar
import android.content.Context import android.content.Context
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.annotation.VisibleForTesting.PRIVATE
import androidx.core.content.ContextCompat.getColor import androidx.core.content.ContextCompat.getColor
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -21,6 +22,7 @@ import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuImageTextCheckboxButton
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem import mozilla.components.browser.menu.item.WebExtensionPlaceholderMenuItem
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
@ -33,13 +35,19 @@ import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.FeatureFlags import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.FeatureFlags.tabsTrayRewrite
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
import org.mozilla.fenix.ext.asActivity import org.mozilla.fenix.ext.asActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.utils.BrowsersCache
/** /**
* Builds the toolbar object used with the 3-dot menu in the browser fragment. * Builds the toolbar object used with the 3-dot menu in the browser fragment.
@ -50,13 +58,12 @@ import org.mozilla.fenix.theme.ThemeManager
* @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs. * @param lifecycleOwner View lifecycle owner used to determine when to cancel UI jobs.
* @param bookmarksStorage Used to check if a page is bookmarked. * @param bookmarksStorage Used to check if a page is bookmarked.
*/ */
@Suppress("LargeClass", "LongParameterList") @Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
@ExperimentalCoroutinesApi @ExperimentalCoroutinesApi
class DefaultToolbarMenu( open class DefaultToolbarMenu(
private val context: Context, private val context: Context,
private val store: BrowserStore, private val store: BrowserStore,
hasAccountProblem: Boolean = false, hasAccountProblem: Boolean = false,
shouldReverseItems: Boolean,
private val onItemTapped: (ToolbarMenu.Item) -> Unit = {}, private val onItemTapped: (ToolbarMenu.Item) -> Unit = {},
private val lifecycleOwner: LifecycleOwner, private val lifecycleOwner: LifecycleOwner,
private val bookmarksStorage: BookmarksStorage, private val bookmarksStorage: BookmarksStorage,
@ -65,7 +72,11 @@ class DefaultToolbarMenu(
private var isCurrentUrlBookmarked = false private var isCurrentUrlBookmarked = false
private var isBookmarkedJob: Job? = null private var isBookmarkedJob: Job? = null
private val isTopToolbarSelected = shouldReverseItems
private val shouldDeleteDataOnQuit = context.settings().shouldDeleteBrowsingDataOnQuit
private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
private val accountManager = FenixAccountManager(context)
private val selectedSession: TabSessionState? private val selectedSession: TabSessionState?
get() = store.state.selectedTab get() = store.state.selectedTab
@ -77,13 +88,16 @@ class DefaultToolbarMenu(
} else { } else {
oldCoreMenuItems oldCoreMenuItems
}, },
endOfMenuAlwaysVisible = !shouldReverseItems, endOfMenuAlwaysVisible = shouldUseBottomToolbar,
store = store, store = store,
webExtIconTintColorResource = primaryTextColor(), style = WebExtensionBrowserMenuBuilder.Style(
webExtIconTintColorResource = primaryTextColor(),
addonsManagerMenuItemDrawableRes = R.drawable.ic_addons_extensions
),
onAddonsManagerTapped = { onAddonsManagerTapped = {
onItemTapped.invoke(ToolbarMenu.Item.AddonsManager) onItemTapped.invoke(ToolbarMenu.Item.AddonsManager)
}, },
appendExtensionSubMenuAtStart = !shouldReverseItems appendExtensionSubMenuAtStart = shouldUseBottomToolbar
) )
} }
@ -137,7 +151,7 @@ class DefaultToolbarMenu(
} }
val share = BrowserMenuItemToolbar.Button( val share = BrowserMenuItemToolbar.Button(
imageResource = R.drawable.ic_share_filled, imageResource = R.drawable.ic_share,
contentDescription = context.getString(R.string.browser_menu_share), contentDescription = context.getString(R.string.browser_menu_share),
iconTintColorResource = primaryTextColor(), iconTintColorResource = primaryTextColor(),
listener = { listener = {
@ -148,7 +162,7 @@ class DefaultToolbarMenu(
registerForIsBookmarkedUpdates() registerForIsBookmarkedUpdates()
if (FeatureFlags.toolbarMenuFeature) { if (FeatureFlags.toolbarMenuFeature) {
BrowserMenuItemToolbar(listOf(back, forward, share, refresh)) BrowserMenuItemToolbar(listOf(back, forward, share, refresh), isSticky = true)
} else { } else {
val bookmark = BrowserMenuItemToolbar.TwoStateButton( val bookmark = BrowserMenuItemToolbar.TwoStateButton(
primaryImageResource = R.drawable.ic_bookmark_filled, primaryImageResource = R.drawable.ic_bookmark_filled,
@ -163,8 +177,7 @@ class DefaultToolbarMenu(
secondaryImageTintResource = primaryTextColor(), secondaryImageTintResource = primaryTextColor(),
disableInSecondaryState = false disableInSecondaryState = false
) { ) {
if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true handleBookmarkItemTapped()
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
} }
BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh)) BrowserMenuItemToolbar(listOf(back, forward, bookmark, share, refresh))
@ -172,24 +185,43 @@ class DefaultToolbarMenu(
} }
// Predicates that need to be repeatedly called as the session changes // Predicates that need to be repeatedly called as the session changes
private fun canAddToHomescreen(): Boolean = @VisibleForTesting(otherwise = PRIVATE)
fun canAddToHomescreen(): Boolean =
selectedSession != null && isPinningSupported && selectedSession != null && isPinningSupported &&
!context.components.useCases.webAppUseCases.isInstallable() !context.components.useCases.webAppUseCases.isInstallable()
private fun canInstall(): Boolean = @VisibleForTesting(otherwise = PRIVATE)
fun canInstall(): Boolean =
selectedSession != null && isPinningSupported && selectedSession != null && isPinningSupported &&
context.components.useCases.webAppUseCases.isInstallable() context.components.useCases.webAppUseCases.isInstallable()
private fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session -> @VisibleForTesting(otherwise = PRIVATE)
fun shouldShowOpenInApp(): Boolean = selectedSession?.let { session ->
val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect val appLink = context.components.useCases.appLinksUseCases.appLinkRedirect
appLink(session.content.url).hasExternalApp() appLink(session.content.url).hasExternalApp()
} ?: false } ?: false
private fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let { @VisibleForTesting(otherwise = PRIVATE)
fun shouldShowReaderViewCustomization(): Boolean = selectedSession?.let {
store.state.findTab(it.id)?.readerState?.active store.state.findTab(it.id)?.readerState?.active
} ?: false } ?: false
// End of predicates // // End of predicates //
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.InstallPwaToHomeScreen)
}
private val oldCoreMenuItems by lazy { private val oldCoreMenuItems by lazy {
val settings = BrowserMenuHighlightableItem( val settings = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_settings), label = context.getString(R.string.browser_menu_settings),
@ -244,21 +276,6 @@ class DefaultToolbarMenu(
onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs) 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( val findInPage = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page), label = context.getString(R.string.browser_menu_find_in_page),
imageResource = R.drawable.mozac_ic_search, imageResource = R.drawable.mozac_ic_search,
@ -348,6 +365,9 @@ class DefaultToolbarMenu(
BrowserMenuDivider(), BrowserMenuDivider(),
reportSiteIssuePlaceholder, reportSiteIssuePlaceholder,
findInPage, findInPage,
getSetDefaultBrowserItem()?.let { BrowserMenuDivider() },
getSetDefaultBrowserItem(),
getSetDefaultBrowserItem()?.let { BrowserMenuDivider() },
addToTopSites, addToTopSites,
addToHomescreen.apply { visible = ::canAddToHomescreen }, addToHomescreen.apply { visible = ::canAddToHomescreen },
installToHomescreen.apply { visible = ::canInstall }, installToHomescreen.apply { visible = ::canInstall },
@ -359,157 +379,195 @@ class DefaultToolbarMenu(
menuToolbar menuToolbar
) )
if (shouldReverseItems) { if (shouldUseBottomToolbar) {
menuItems.reversed()
} else {
menuItems menuItems
} else {
menuItems.reversed()
} }
} }
private val newCoreMenuItems by lazy { val newTabItem = BrowserMenuImageText(
val newTabItem = BrowserMenuImageText( context.getString(R.string.library_new_tab),
context.getString(R.string.library_new_tab), R.drawable.ic_new,
R.drawable.ic_new, primaryTextColor()
primaryTextColor() ) {
) { onItemTapped.invoke(ToolbarMenu.Item.NewTab)
onItemTapped.invoke(ToolbarMenu.Item.NewTab) }
}
val bookmarksItem = BrowserMenuImageText( val historyItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks), context.getString(R.string.library_history),
R.drawable.ic_bookmark_filled, R.drawable.ic_history,
primaryTextColor() primaryTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.Bookmarks) onItemTapped.invoke(ToolbarMenu.Item.History)
} }
val historyItem = BrowserMenuImageText( val downloadsItem = BrowserMenuImageText(
context.getString(R.string.library_history), context.getString(R.string.library_downloads),
R.drawable.ic_history, R.drawable.ic_download,
primaryTextColor() primaryTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.History) onItemTapped.invoke(ToolbarMenu.Item.Downloads)
} }
val downloadsItem = BrowserMenuImageText( val extensionsItem = WebExtensionPlaceholderMenuItem(
context.getString(R.string.library_downloads), id = WebExtensionPlaceholderMenuItem.MAIN_EXTENSIONS_MENU_ID
R.drawable.ic_download, )
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.Downloads)
}
val extensionsItem = BrowserMenuImageText( val findInPageItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_extensions), label = context.getString(R.string.browser_menu_find_in_page),
R.drawable.ic_addons_extensions, imageResource = R.drawable.mozac_ic_search,
primaryTextColor() iconTintColorResource = primaryTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.AddonsManager) onItemTapped.invoke(ToolbarMenu.Item.FindInPage)
} }
val syncedTabs = BrowserMenuImageText( val desktopSiteItem = BrowserMenuImageSwitch(
label = context.getString(R.string.synced_tabs), imageResource = R.drawable.ic_desktop,
imageResource = R.drawable.ic_synced_tabs, label = context.getString(R.string.browser_menu_desktop_site),
iconTintColorResource = primaryTextColor() initialState = {
) { selectedSession?.content?.desktopMode ?: false
onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
} }
) { checked ->
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked))
}
val findInPageItem = BrowserMenuImageText( val customizeReaderView = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_find_in_page), label = context.getString(R.string.browser_menu_customize_reader_view),
imageResource = R.drawable.mozac_ic_search, imageResource = R.drawable.ic_readermode_appearance,
iconTintColorResource = primaryTextColor() iconTintColorResource = primaryTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.FindInPage) onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView)
} }
val desktopSiteItem = BrowserMenuImageSwitch( val openInApp = BrowserMenuHighlightableItem(
imageResource = R.drawable.ic_desktop, label = context.getString(R.string.browser_menu_open_app_link),
label = context.getString(R.string.browser_menu_desktop_site), startImageResource = R.drawable.ic_open_in_app,
initialState = { iconTintColorResource = primaryTextColor(),
selectedSession?.content?.desktopMode ?: false highlight = BrowserMenuHighlight.LowPriority(
} label = context.getString(R.string.browser_menu_open_app_link),
) { checked -> notificationTint = getColor(context, R.color.whats_new_notification_color)
onItemTapped.invoke(ToolbarMenu.Item.RequestDesktop(checked)) ),
} isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val customizeReaderView = BrowserMenuImageText( val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem(
label = context.getString(R.string.browser_menu_customize_reader_view), id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID
imageResource = R.drawable.ic_readermode_appearance, )
iconTintColorResource = primaryTextColor()
) { val addToHomeScreenItem = BrowserMenuImageText(
onItemTapped.invoke(ToolbarMenu.Item.CustomizeReaderView) label = context.getString(R.string.browser_menu_add_to_homescreen),
} imageResource = R.drawable.ic_add_to_homescreen,
iconTintColorResource = primaryTextColor(),
isCollapsingMenuLimit = true
) {
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen)
}
val openInApp = BrowserMenuHighlightableItem( val addToTopSitesItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_open_app_link), label = context.getString(R.string.browser_menu_add_to_top_sites),
startImageResource = R.drawable.ic_open_in_app, imageResource = R.drawable.ic_top_sites,
iconTintColorResource = primaryTextColor(), iconTintColorResource = primaryTextColor()
highlight = BrowserMenuHighlight.LowPriority( ) {
label = context.getString(R.string.browser_menu_open_app_link), onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites)
notificationTint = getColor(context, R.color.whats_new_notification_color) }
),
isHighlighted = { !context.settings().openInAppOpened }
) {
onItemTapped.invoke(ToolbarMenu.Item.OpenInApp)
}
val reportSiteIssuePlaceholder = WebExtensionPlaceholderMenuItem( val saveToCollectionItem = BrowserMenuImageText(
id = WebCompatReporterFeature.WEBCOMPAT_REPORTER_EXTENSION_ID 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 addToHomeScreenItem = BrowserMenuImageText( val settingsItem = BrowserMenuHighlightableItem(
label = context.getString(R.string.browser_menu_add_to_homescreen), label = context.getString(R.string.browser_menu_settings),
imageResource = R.drawable.ic_add_to_homescreen, startImageResource = R.drawable.ic_settings,
iconTintColorResource = primaryTextColor() iconTintColorResource = if (hasAccountProblem)
) { ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) else
onItemTapped.invoke(ToolbarMenu.Item.AddToHomeScreen) 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 addToTopSitesItem = BrowserMenuImageText( val bookmarksItem = BrowserMenuImageTextCheckboxButton(
label = context.getString(R.string.browser_menu_add_to_top_sites), imageResource = R.drawable.ic_bookmarks_menu,
imageResource = R.drawable.ic_top_sites, iconTintColorResource = primaryTextColor(),
iconTintColorResource = primaryTextColor() label = context.getString(R.string.library_bookmarks),
) { labelListener = {
onItemTapped.invoke(ToolbarMenu.Item.AddToTopSites) onItemTapped.invoke(ToolbarMenu.Item.Bookmarks)
} },
primaryStateIconResource = R.drawable.ic_bookmark_outline,
secondaryStateIconResource = R.drawable.ic_bookmark_filled,
tintColorResource = menuItemButtonTintColor(),
primaryLabel = context.getString(R.string.browser_menu_add),
secondaryLabel = context.getString(R.string.browser_menu_edit),
isInPrimaryState = { !isCurrentUrlBookmarked }
) {
handleBookmarkItemTapped()
}
val saveToCollectionItem = BrowserMenuImageText( val deleteDataOnQuit = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_save_to_collection_2), label = context.getString(R.string.delete_browsing_data_on_quit_action),
imageResource = R.drawable.ic_tab_collection, imageResource = R.drawable.ic_exit,
iconTintColorResource = primaryTextColor() iconTintColorResource = primaryTextColor()
) { ) {
onItemTapped.invoke(ToolbarMenu.Item.SaveToCollection) onItemTapped.invoke(ToolbarMenu.Item.Quit)
} }
val settingsItem = BrowserMenuHighlightableItem( val syncedTabsItem = BrowserMenuImageText(
label = context.getString(R.string.browser_menu_settings), context.getString(R.string.synced_tabs),
startImageResource = R.drawable.ic_settings, R.drawable.ic_synced_tabs,
iconTintColorResource = primaryTextColor(), primaryTextColor()
textColorResource = if (hasAccountProblem) ) {
ThemeManager.resolveAttribute(R.attr.primaryText, context) else onItemTapped.invoke(ToolbarMenu.Item.SyncedTabs)
primaryTextColor(), }
highlight = BrowserMenuHighlight.HighPriority(
endImageResource = R.drawable.ic_sync_disconnected, private fun getSyncItemTitle(): String {
backgroundTint = context.getColorFromAttr(R.attr.syncDisconnectedBackground), val authenticatedAccount = accountManager.authenticatedAccount
canPropagate = false val email = accountManager.accountProfileEmail
),
isHighlighted = { hasAccountProblem } return if (authenticatedAccount && !email.isNullOrEmpty()) {
) { email
onItemTapped.invoke(ToolbarMenu.Item.Settings) } else {
context.getString(R.string.sync_menu_sign_in)
} }
}
val syncMenuItem = BrowserMenuImageText(
getSyncItemTitle(),
R.drawable.ic_signed_out,
primaryTextColor()
) {
onItemTapped.invoke(ToolbarMenu.Item.SyncAccount(accountManager.signedInToFxa()))
}
@VisibleForTesting(otherwise = PRIVATE)
val newCoreMenuItems by lazy {
val menuItems = val menuItems =
listOfNotNull( listOfNotNull(
if (isTopToolbarSelected) menuToolbar else null, if (shouldUseBottomToolbar) null else menuToolbar,
newTabItem, newTabItem,
BrowserMenuDivider(), BrowserMenuDivider(),
bookmarksItem, bookmarksItem,
historyItem, historyItem,
downloadsItem, downloadsItem,
extensionsItem, extensionsItem,
syncedTabs, if (tabsTrayRewrite) syncMenuItem else syncedTabsItem,
BrowserMenuDivider(), BrowserMenuDivider(),
getSetDefaultBrowserItem(),
getSetDefaultBrowserItem()?.let { BrowserMenuDivider() },
findInPageItem, findInPageItem,
desktopSiteItem, desktopSiteItem,
customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization }, customizeReaderView.apply { visible = ::shouldShowReaderViewCustomization },
@ -517,21 +575,32 @@ class DefaultToolbarMenu(
reportSiteIssuePlaceholder, reportSiteIssuePlaceholder,
BrowserMenuDivider(), BrowserMenuDivider(),
addToHomeScreenItem.apply { visible = ::canAddToHomescreen }, addToHomeScreenItem.apply { visible = ::canAddToHomescreen },
installToHomescreen.apply { visible = ::canInstall },
addToTopSitesItem, addToTopSitesItem,
saveToCollectionItem, saveToCollectionItem,
BrowserMenuDivider(), BrowserMenuDivider(),
settingsItem, settingsItem,
if (isTopToolbarSelected) null else BrowserMenuDivider(), if (shouldDeleteDataOnQuit) deleteDataOnQuit else null,
if (isTopToolbarSelected) null else menuToolbar if (shouldUseBottomToolbar) BrowserMenuDivider() else null,
if (shouldUseBottomToolbar) menuToolbar else null
) )
menuItems menuItems
} }
private fun handleBookmarkItemTapped() {
if (!isCurrentUrlBookmarked) isCurrentUrlBookmarked = true
onItemTapped.invoke(ToolbarMenu.Item.Bookmark)
}
@ColorRes @ColorRes
@VisibleForTesting @VisibleForTesting
internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context) internal fun primaryTextColor() = ThemeManager.resolveAttribute(R.attr.primaryText, context)
@ColorRes
@VisibleForTesting
internal fun menuItemButtonTintColor() = ThemeManager.resolveAttribute(R.attr.menuItemButtonTintColor, context)
@VisibleForTesting @VisibleForTesting
internal fun registerForIsBookmarkedUpdates() { internal fun registerForIsBookmarkedUpdates() {
store.flowScoped(lifecycleOwner) { flow -> store.flowScoped(lifecycleOwner) { flow ->
@ -558,4 +627,24 @@ class DefaultToolbarMenu(
.any { it.url == newUrl } .any { it.url == newUrl }
} }
} }
private fun getSetDefaultBrowserItem(): BrowserMenuImageText? {
val experiments = context.components.analytics.experiments
val browsers = BrowsersCache.all(context)
return experiments.withExperiment(Experiments.DEFAULT_BROWSER) { experimentBranch ->
if (experimentBranch == ExperimentBranch.DEFAULT_BROWSER_TOOLBAR_MENU &&
!browsers.isFirefoxDefaultBrowser
) {
return@withExperiment BrowserMenuImageText(
label = context.getString(R.string.preferences_set_as_default_browser),
imageResource = R.mipmap.ic_launcher
) {
onItemTapped.invoke(ToolbarMenu.Item.SetDefaultBrowser)
}
} else {
null
}
}
}
} }

@ -20,12 +20,14 @@ interface ToolbarMenu {
object OpenInFenix : Item() object OpenInFenix : Item()
object SaveToCollection : Item() object SaveToCollection : Item()
object AddToTopSites : Item() object AddToTopSites : Item()
object InstallToHomeScreen : Item() object InstallPwaToHomeScreen : Item()
object AddToHomeScreen : Item() object AddToHomeScreen : Item()
object SyncedTabs : Item() object SyncedTabs : Item()
data class SyncAccount(val signedIn: Boolean) : Item()
object AddonsManager : Item() object AddonsManager : Item()
object Quit : Item() object Quit : Item()
object OpenInApp : Item() object OpenInApp : Item()
object SetDefaultBrowser : Item()
object Bookmark : Item() object Bookmark : Item()
object CustomizeReaderView : Item() object CustomizeReaderView : Item()
object Bookmarks : Item() object Bookmarks : Item()

@ -60,6 +60,9 @@ open class ExternalAppBrowserActivity : HomeActivity() {
homeActivityInitTimeStampNanoSeconds, homeActivityInitTimeStampNanoSeconds,
rootContainer rootContainer
) )
// coldStartupDurationTelemetry.onHomeActivityOnCreate is intentionally omitted so we don't
// include even more unpredictable code paths in the results.
} }
override fun navigateToBrowserOnColdStart() { override fun navigateToBrowserOnColdStart() {

@ -50,7 +50,8 @@ class DynamicDownloadDialogBehavior<V : View>(
/** /**
* Reference to [EngineView] used to check user's [android.view.MotionEvent]s. * Reference to [EngineView] used to check user's [android.view.MotionEvent]s.
*/ */
private var engineView: EngineView? = null @VisibleForTesting
internal var engineView: EngineView? = null
/** /**
* Depending on how user's touch was consumed by EngineView / current website, * Depending on how user's touch was consumed by EngineView / current website,
@ -64,7 +65,9 @@ class DynamicDownloadDialogBehavior<V : View>(
*/ */
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val shouldScroll: Boolean internal val shouldScroll: Boolean
get() = engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_HANDLED get() = engineView?.getInputResultDetail()?.let {
(it.canScrollToBottom() || it.canScrollToTop())
} ?: false
override fun onStartNestedScroll( override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout, coordinatorLayout: CoordinatorLayout,
@ -78,7 +81,7 @@ class DynamicDownloadDialogBehavior<V : View>(
shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH
snapAnimator.cancel() snapAnimator.cancel()
true true
} else if (engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_UNHANDLED) { } else if (engineView?.getInputResultDetail()?.isTouchUnhandled() == true) {
// Force expand the notification dialog if event is unhandled, otherwise user could get stuck in a // Force expand the notification dialog if event is unhandled, otherwise user could get stuck in a
// state where they cannot show it // state where they cannot show it
forceExpand(child) forceExpand(child)

@ -7,8 +7,8 @@ package org.mozilla.fenix.experiments
class Experiments { class Experiments {
companion object { companion object {
const val A_A_NIMBUS_VALIDATION = "fenix-nimbus-validation-v3" 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" const val ANDROID_KEYSTORE = "fenix-android-keystore"
const val DEFAULT_BROWSER = "fenix-default-browser"
} }
} }
@ -18,5 +18,8 @@ class ExperimentBranch {
const val CONTROL = "control" const val CONTROL = "control"
const val A1 = "a1" const val A1 = "a1"
const val A2 = "a2" const val A2 = "a2"
const val DEFAULT_BROWSER_TOOLBAR_MENU = "default_browser_toolbar_menu"
const val DEFAULT_BROWSER_NEW_TAB_BANNER = "default_browser_newtab_banner"
const val DEFAULT_BROWSER_SETTINGS_MENU = "default_browser_settings_menu"
} }
} }

@ -8,6 +8,14 @@ import android.app.Activity
import android.view.View import android.view.View
import android.view.WindowManager import android.view.WindowManager
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import android.app.role.RoleManager
import android.content.Intent
import android.os.Build
import android.provider.Settings
import androidx.core.os.bundleOf
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.settings.SupportUtils
/** /**
* Attempts to call immersive mode using the View to hide the status bar and navigation buttons. * Attempts to call immersive mode using the View to hide the status bar and navigation buttons.
@ -15,8 +23,17 @@ import mozilla.components.concept.base.crash.Breadcrumb
* We don't use the equivalent function from Android Components because the stable flag messes * We don't use the equivalent function from Android Components because the stable flag messes
* with the toolbar. See #1998 and #3272. * with the toolbar. See #1998 and #3272.
*/ */
@Deprecated(
message = "Use the Android Component implementation instead.",
replaceWith = ReplaceWith(
"enterToImmersiveMode()",
"mozilla.components.support.ktx.android.view.enterToImmersiveMode"
)
)
fun Activity.enterToImmersiveMode() { fun Activity.enterToImmersiveMode() {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
// This will be addressed on https://github.com/mozilla-mobile/fenix/issues/17804
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
@ -24,19 +41,6 @@ fun Activity.enterToImmersiveMode() {
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY) or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
} }
/**
* Attempts to come out from immersive mode using the View.
*/
fun Activity.exitImmersiveModeIfNeeded() {
if (WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON and window.attributes.flags == 0) {
// We left immersive mode already.
return
}
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
}
fun Activity.breadcrumb( fun Activity.breadcrumb(
message: String, message: String,
data: Map<String, String> = emptyMap() data: Map<String, String> = emptyMap()
@ -52,3 +56,59 @@ fun Activity.breadcrumb(
) )
) )
} }
/**
* Opens Android's Manage Default Apps Settings if possible.
*/
fun Activity.openSetDefaultBrowserOption() {
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
getSystemService(RoleManager::class.java).also {
if (it.isRoleAvailable(RoleManager.ROLE_BROWSER) && !it.isRoleHeld(
RoleManager.ROLE_BROWSER
)
) {
startActivityForResult(
it.createRequestRoleIntent(RoleManager.ROLE_BROWSER),
REQUEST_CODE_BROWSER_ROLE
)
} else {
navigateToDefaultBrowserAppsSettings()
}
}
}
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
navigateToDefaultBrowserAppsSettings()
}
else -> {
(this as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
this,
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
),
newTab = true,
from = BrowserDirection.FromSettings
)
}
}
}
private fun Activity.navigateToDefaultBrowserAppsSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val intent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
intent.putExtra(
SETTINGS_SELECT_OPTION_KEY,
DEFAULT_BROWSER_APP_OPTION
)
intent.putExtra(
SETTINGS_SHOW_FRAGMENT_ARGS,
bundleOf(SETTINGS_SELECT_OPTION_KEY to DEFAULT_BROWSER_APP_OPTION)
)
startActivity(intent)
}
}
const val REQUEST_CODE_BROWSER_ROLE = 1
const val SETTINGS_SELECT_OPTION_KEY = ":settings:fragment_args_key"
const val SETTINGS_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args"
const val DEFAULT_BROWSER_APP_OPTION = "default_browser"

@ -2,6 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// We suppress the calls to `navigate` since we invoke the Android `NavController.navigate` through
// this file. Detekt checks for the `navigate()` function calls, which should be ignored in this file.
@file:Suppress("MozillaNavigateCheck")
package org.mozilla.fenix.ext package org.mozilla.fenix.ext
import androidx.annotation.IdRes import androidx.annotation.IdRes
@ -10,12 +13,15 @@ import androidx.navigation.NavDirections
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import io.sentry.Sentry import io.sentry.Sentry
import org.mozilla.fenix.components.isSentryEnabled import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.perf.NavGraphProvider
/** /**
* Navigate from the fragment with [id] using the given [directions]. * Navigate from the fragment with [id] using the given [directions].
* If the id doesn't match the current destination, an error is recorded. * If the id doesn't match the current destination, an error is recorded.
*/ */
fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: NavOptions? = null) { fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: NavOptions? = null) {
NavGraphProvider.blockForNavGraphInflation(this)
if (id == null || this.currentDestination?.id == id) { if (id == null || this.currentDestination?.id == id) {
this.navigate(directions, navOptions) this.navigate(directions, navOptions)
} else { } else {
@ -23,6 +29,21 @@ fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: Na
} }
} }
fun NavController.navigateBlockingForAsyncNavGraph(resId: Int) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(resId)
}
fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(directions)
}
fun NavController.navigateBlockingForAsyncNavGraph(directions: NavDirections, navOptions: NavOptions?) {
NavGraphProvider.blockForNavGraphInflation(this)
this.navigate(directions, navOptions)
}
fun NavController.alreadyOnDestination(@IdRes destId: Int?): Boolean { fun NavController.alreadyOnDestination(@IdRes destId: Int?): Boolean {
return destId?.let { currentDestination?.id == it || popBackStack(it, false) } ?: false return destId?.let { currentDestination?.id == it || popBackStack(it, false) } ?: false
} }
@ -38,6 +59,6 @@ fun NavController.navigateSafe(
directions: NavDirections directions: NavDirections
) { ) {
if (currentDestination?.id == resId) { if (currentDestination?.id == resId) {
this.navigate(directions) this.navigateBlockingForAsyncNavGraph(directions)
} }
} }

@ -7,6 +7,7 @@ package org.mozilla.fenix.ext
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.text.Editable
import android.util.Patterns import android.util.Patterns
import android.webkit.URLUtil import android.webkit.URLUtil
import androidx.core.net.toUri import androidx.core.net.toUri
@ -114,6 +115,11 @@ fun String.simplifiedUrl(): String {
return afterScheme return afterScheme
} }
/**
* Returns an [Editable] for the provided string.
*/
fun String.toEditable(): Editable = Editable.Factory.getInstance().newEditable(this)
suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) { suspend fun bitmapForUrl(url: String, client: Client): Bitmap? = withContext(Dispatchers.IO) {
// Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once, // Code below will cache it in Gecko's cache, which ensures that as long as we've fetched it once,
// we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data'). // we will be able to display this avatar as long as the cache isn't purged (e.g. via 'clear user data').

@ -89,6 +89,7 @@ import org.mozilla.fenix.GleanMetrics.PerfStartup
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions import org.mozilla.fenix.browser.BrowserAnimator.Companion.getToolbarNavOptions
import org.mozilla.fenix.browser.BrowserFragmentDirections
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
import org.mozilla.fenix.components.PrivateShortcutCreateManager import org.mozilla.fenix.components.PrivateShortcutCreateManager
@ -102,6 +103,7 @@ import org.mozilla.fenix.components.toolbar.FenixTabCounterMenu
import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.components.toolbar.ToolbarPosition
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.hideToolbar import org.mozilla.fenix.ext.hideToolbar
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
@ -225,7 +227,8 @@ class HomeFragment : Fragment() {
) )
).getTip() ).getTip()
}, },
showCollectionPlaceholder = false showCollectionPlaceholder = components.settings.showCollectionsPlaceholderOnHome,
showSetAsDefaultBrowserCard = components.settings.shouldShowSetAsDefaultBrowserCard()
) )
) )
} }
@ -252,6 +255,7 @@ class HomeFragment : Fragment() {
restoreUseCase = components.useCases.tabsUseCases.restore, restoreUseCase = components.useCases.tabsUseCases.restore,
reloadUrlUseCase = components.useCases.sessionUseCases.reload, reloadUrlUseCase = components.useCases.sessionUseCases.reload,
selectTabUseCase = components.useCases.tabsUseCases.selectTab, selectTabUseCase = components.useCases.tabsUseCases.selectTab,
requestDesktopSiteUseCase = components.useCases.sessionUseCases.requestDesktopSite,
fragmentStore = homeFragmentStore, fragmentStore = homeFragmentStore,
navController = findNavController(), navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
@ -359,82 +363,83 @@ class HomeFragment : Fragment() {
@Suppress("LongMethod", "ComplexMethod") @Suppress("LongMethod", "ComplexMethod")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) = override fun onViewCreated(view: View, savedInstanceState: Bundle?) =
PerfStartup.homeFragmentOnViewCreated.measureNoInline { // weird indent so we don't have to break blame. PerfStartup.homeFragmentOnViewCreated.measureNoInline {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
context?.metrics?.track(Event.HomeScreenDisplayed)
observeSearchEngineChanges()
createHomeMenu(requireContext(), WeakReference(view.menuButton)) observeSearchEngineChanges()
createTabCounterMenu(view) createHomeMenu(requireContext(), WeakReference(view.menuButton))
createTabCounterMenu(view)
view.menuButton.setColorFilter(
ContextCompat.getColor( view.menuButton.setColorFilter(
requireContext(), ContextCompat.getColor(
ThemeManager.resolveAttribute(R.attr.primaryText, requireContext()) requireContext(),
ThemeManager.resolveAttribute(R.attr.primaryText, requireContext())
)
) )
)
view.toolbar.compoundDrawablePadding = view.toolbar.compoundDrawablePadding =
view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding) view.resources.getDimensionPixelSize(R.dimen.search_bar_search_engine_icon_padding)
view.toolbar_wrapper.setOnClickListener { view.toolbar_wrapper.setOnClickListener {
navigateToSearch() navigateToSearch()
requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME)) requireComponents.analytics.metrics.track(Event.SearchBarTapped(Event.SearchBarTapped.Source.HOME))
} }
view.toolbar_wrapper.setOnLongClickListener {
ToolbarPopupWindow.show(
WeakReference(it),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste,
copyVisible = false
)
true
}
view.tab_button.setOnClickListener { view.toolbar_wrapper.setOnLongClickListener {
openTabTray() ToolbarPopupWindow.show(
} WeakReference(it),
handlePasteAndGo = sessionControlInteractor::onPasteAndGo,
handlePaste = sessionControlInteractor::onPaste,
copyVisible = false
)
true
}
PrivateBrowsingButtonView( view.tab_button.setOnClickListener {
privateBrowsingButton, openTabTray()
browsingModeManager
) { newMode ->
if (newMode == BrowsingMode.Private) {
requireContext().settings().incrementNumTimesPrivateModeOpened()
} }
if (onboarding.userHasBeenOnboarded()) { PrivateBrowsingButtonView(
homeFragmentStore.dispatch( privateBrowsingButton,
HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode)) browsingModeManager
) ) { newMode ->
if (newMode == BrowsingMode.Private) {
requireContext().settings().incrementNumTimesPrivateModeOpened()
}
if (onboarding.userHasBeenOnboarded()) {
homeFragmentStore.dispatch(
HomeFragmentAction.ModeChange(Mode.fromBrowsingMode(newMode))
)
}
} }
}
consumeFrom(requireComponents.core.store) { consumeFrom(requireComponents.core.store) {
updateTabCounter(it) updateTabCounter(it)
} }
homeViewModel.sessionToDelete?.also { homeViewModel.sessionToDelete?.also {
if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) { if (it == ALL_NORMAL_TABS || it == ALL_PRIVATE_TABS) {
removeAllTabsAndShowSnackbar(it) removeAllTabsAndShowSnackbar(it)
} else { } else {
removeTabAndShowSnackbar(it) removeTabAndShowSnackbar(it)
}
} }
}
homeViewModel.sessionToDelete = null homeViewModel.sessionToDelete = null
updateTabCounter(requireComponents.core.store.state) updateTabCounter(requireComponents.core.store.state)
if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) { if (bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR)) {
navigateToSearch() navigateToSearch()
} else if (bundleArgs.getLong(FOCUS_ON_COLLECTION, -1) >= 0) { } else if (bundleArgs.getLong(FOCUS_ON_COLLECTION, -1) >= 0) {
// No need to scroll to async'd loaded TopSites if we want to scroll to collections. // No need to scroll to async'd loaded TopSites if we want to scroll to collections.
homeViewModel.shouldScrollToTopSites = false homeViewModel.shouldScrollToTopSites = false
/* Triggered when the user has added a tab to a collection and has tapped /* Triggered when the user has added a tab to a collection and has tapped
* the View action on the [TabsTrayDialogFragment] snackbar.*/ * the View action on the [TabsTrayDialogFragment] snackbar.*/
scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1)) scrollAndAnimateCollection(bundleArgs.getLong(FOCUS_ON_COLLECTION, -1))
}
} }
}
private fun observeSearchEngineChanges() { private fun observeSearchEngineChanges() {
consumeFlow(store) { flow -> consumeFlow(store) { flow ->
@ -531,7 +536,7 @@ class HomeFragment : Fragment() {
requireContext().getString(R.string.snackbar_deleted_undo), requireContext().getString(R.string.snackbar_deleted_undo),
{ {
requireComponents.useCases.tabsUseCases.undo.invoke() requireComponents.useCases.tabsUseCases.undo.invoke()
findNavController().navigate( findNavController().navigateBlockingForAsyncNavGraph(
HomeFragmentDirections.actionGlobalBrowser(null) HomeFragmentDirections.actionGlobalBrowser(null)
) )
}, },
@ -622,7 +627,8 @@ class HomeFragment : Fragment() {
} }
private fun navToSavedLogins() { private fun navToSavedLogins() {
findNavController().navigate(HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment()) findNavController().navigateBlockingForAsyncNavGraph(
HomeFragmentDirections.actionGlobalSavedLoginsAuthFragment())
} }
private fun dispatchModeChanges(mode: Mode) { private fun dispatchModeChanges(mode: Mode) {
@ -738,6 +744,7 @@ class HomeFragment : Fragment() {
private fun hideOnboardingAndOpenSearch() { private fun hideOnboardingAndOpenSearch() {
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
appBarLayout?.setExpanded(true, true)
navigateToSearch() navigateToSearch()
} }
@ -777,14 +784,27 @@ class HomeFragment : Fragment() {
R.id.homeFragment, R.id.homeFragment,
HomeFragmentDirections.actionGlobalSettingsFragment() HomeFragmentDirections.actionGlobalSettingsFragment()
) )
requireComponents.analytics.metrics.track(Event.HomeMenuSettingsItemClicked)
} }
HomeMenu.Item.SyncedTabs -> { HomeMenu.Item.SyncTabs -> {
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
nav( nav(
R.id.homeFragment, R.id.homeFragment,
HomeFragmentDirections.actionGlobalSyncedTabsFragment() HomeFragmentDirections.actionGlobalSyncedTabsFragment()
) )
} }
is HomeMenu.Item.SyncAccount -> {
hideOnboardingIfNeeded()
val directions = if (it.signedIn) {
BrowserFragmentDirections.actionGlobalAccountSettingsFragment()
} else {
BrowserFragmentDirections.actionGlobalTurnOnSync()
}
nav(
R.id.homeFragment,
directions
)
}
HomeMenu.Item.Bookmarks -> { HomeMenu.Item.Bookmarks -> {
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
nav( nav(
@ -841,19 +861,22 @@ class HomeFragment : Fragment() {
} }
) )
} }
HomeMenu.Item.Sync -> { HomeMenu.Item.ReconnectSync -> {
hideOnboardingIfNeeded() hideOnboardingIfNeeded()
nav( nav(
R.id.homeFragment, R.id.homeFragment,
HomeFragmentDirections.actionGlobalAccountProblemFragment() HomeFragmentDirections.actionGlobalAccountProblemFragment()
) )
} }
HomeMenu.Item.AddonsManager -> { HomeMenu.Item.Extensions -> {
nav( nav(
R.id.homeFragment, R.id.homeFragment,
HomeFragmentDirections.actionGlobalAddonsManagementFragment() HomeFragmentDirections.actionGlobalAddonsManagementFragment()
) )
} }
is HomeMenu.Item.DesktopMode -> {
context.settings().openNextTabInDesktopMode = it.checked
}
} }
}, },
onHighlightPresent = { menuButtonView.get()?.setHighlight(it) }, onHighlightPresent = { menuButtonView.get()?.setHighlight(it) },

@ -48,7 +48,8 @@ data class HomeFragmentState(
val mode: Mode, val mode: Mode,
val topSites: List<TopSite>, val topSites: List<TopSite>,
val tip: Tip? = null, val tip: Tip? = null,
val showCollectionPlaceholder: Boolean val showCollectionPlaceholder: Boolean,
val showSetAsDefaultBrowserCard: Boolean
) : State ) : State
sealed class HomeFragmentAction : Action { sealed class HomeFragmentAction : Action {
@ -69,6 +70,7 @@ sealed class HomeFragmentAction : Action {
data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction() data class TopSitesChange(val topSites: List<TopSite>) : HomeFragmentAction()
data class RemoveTip(val tip: Tip) : HomeFragmentAction() data class RemoveTip(val tip: Tip) : HomeFragmentAction()
object RemoveCollectionsPlaceholder : HomeFragmentAction() object RemoveCollectionsPlaceholder : HomeFragmentAction()
object RemoveSetDefaultBrowserCard : HomeFragmentAction()
} }
private fun homeFragmentStateReducer( private fun homeFragmentStateReducer(
@ -102,5 +104,6 @@ private fun homeFragmentStateReducer(
is HomeFragmentAction.RemoveCollectionsPlaceholder -> { is HomeFragmentAction.RemoveCollectionsPlaceholder -> {
state.copy(showCollectionPlaceholder = false) state.copy(showCollectionPlaceholder = false)
} }
is HomeFragmentAction.RemoveSetDefaultBrowserCard -> state.copy(showSetAsDefaultBrowserCard = false)
} }
} }

@ -13,15 +13,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuHighlight import mozilla.components.browser.menu.BrowserMenuHighlight
import mozilla.components.browser.menu.BrowserMenuItem
import mozilla.components.browser.menu.ext.getHighlight import mozilla.components.browser.menu.ext.getHighlight
import mozilla.components.browser.menu.item.BrowserMenuDivider import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
import mozilla.components.support.ktx.android.content.getColorFromAttr import mozilla.components.support.ktx.android.content.getColorFromAttr
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.FeatureFlags.tabsTrayRewrite
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.ExperimentBranch import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments import org.mozilla.fenix.experiments.Experiments
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
@ -30,6 +35,7 @@ import org.mozilla.fenix.ext.withExperiment
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import org.mozilla.fenix.whatsnew.WhatsNew import org.mozilla.fenix.whatsnew.WhatsNew
@Suppress("LargeClass", "LongMethod")
class HomeMenu( class HomeMenu(
private val lifecycleOwner: LifecycleOwner, private val lifecycleOwner: LifecycleOwner,
private val context: Context, private val context: Context,
@ -38,26 +44,28 @@ class HomeMenu(
private val onHighlightPresent: (BrowserMenuHighlight) -> Unit = {} private val onHighlightPresent: (BrowserMenuHighlight) -> Unit = {}
) { ) {
sealed class Item { sealed class Item {
object Bookmarks : Item()
object History : Item()
object Downloads : Item()
object Extensions : Item()
object SyncTabs : Item()
data class SyncAccount(val signedIn: Boolean) : Item()
object WhatsNew : Item() object WhatsNew : Item()
object Help : Item() object Help : Item()
object AddonsManager : Item()
object Settings : Item() object Settings : Item()
object SyncedTabs : Item()
object History : Item()
object Bookmarks : Item()
object Downloads : Item()
object Quit : Item() object Quit : Item()
object Sync : Item() object ReconnectSync : Item()
data class DesktopMode(val checked: Boolean) : Item()
} }
private val primaryTextColor = private val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
ThemeManager.resolveAttribute(R.attr.primaryText, context) private val syncDisconnectedColor =
private val syncDisconnectedColor = ThemeManager.resolveAttribute(R.attr.syncDisconnected, context) ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
private val syncDisconnectedBackgroundColor = context.getColorFromAttr(R.attr.syncDisconnectedBackground) private val syncDisconnectedBackgroundColor =
context.getColorFromAttr(R.attr.syncDisconnectedBackground)
private val menuCategoryTextColor =
ThemeManager.resolveAttribute(R.attr.menuCategoryText, context)
private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar private val shouldUseBottomToolbar = context.settings().shouldUseBottomToolbar
private val accountManager = FenixAccountManager(context)
// 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary. // 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary.
private val reconnectToSyncItem by lazy { private val reconnectToSyncItem by lazy {
@ -72,7 +80,7 @@ class HomeMenu(
), ),
isHighlighted = { true } isHighlighted = { true }
) { ) {
onItemTapped.invoke(Item.Sync) onItemTapped.invoke(Item.ReconnectSync)
} }
} }
@ -86,7 +94,34 @@ class HomeMenu(
} }
} }
private val coreMenuItems by lazy { val syncedTabsItem = BrowserMenuImageText(
context.getString(R.string.synced_tabs),
R.drawable.ic_synced_tabs,
primaryTextColor
) {
onItemTapped.invoke(Item.SyncTabs)
}
private fun getSyncItemTitle(): String {
val authenticatedAccount = accountManager.authenticatedAccount
val email = accountManager.accountProfileEmail
return if (authenticatedAccount && !email.isNullOrEmpty()) {
email
} else {
context.getString(R.string.sync_menu_sign_in)
}
}
val syncSignInMenuItem = BrowserMenuImageText(
getSyncItemTitle(),
R.drawable.ic_synced_tabs,
primaryTextColor
) {
onItemTapped.invoke(Item.SyncAccount(accountManager.signedInToFxa()))
}
private val oldCoreMenuItems by lazy {
val whatsNewItem = BrowserMenuHighlightableItem( val whatsNewItem = BrowserMenuHighlightableItem(
context.getString(R.string.browser_menu_whats_new), context.getString(R.string.browser_menu_whats_new),
R.drawable.ic_whats_new, R.drawable.ic_whats_new,
@ -98,17 +133,10 @@ class HomeMenu(
) { ) {
onItemTapped.invoke(Item.WhatsNew) onItemTapped.invoke(Item.WhatsNew)
} }
val experiments = context.components.analytics.experiments val experiments = context.components.analytics.experiments
val bookmarksIcon = experiments.withExperiment(Experiments.BOOKMARK_ICON) {
when (it) {
ExperimentBranch.TREATMENT -> R.drawable.ic_bookmark_list
else -> R.drawable.ic_bookmark_filled
}
}
val bookmarksItem = BrowserMenuImageText( val bookmarksItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks), context.getString(R.string.library_bookmarks),
bookmarksIcon, R.drawable.ic_bookmark_list,
primaryTextColor primaryTextColor
) { ) {
onItemTapped.invoke(Item.Bookmarks) onItemTapped.invoke(Item.Bookmarks)
@ -141,7 +169,7 @@ class HomeMenu(
R.drawable.ic_addons_extensions, R.drawable.ic_addons_extensions,
primaryTextColor primaryTextColor
) { ) {
onItemTapped.invoke(Item.AddonsManager) onItemTapped.invoke(Item.Extensions)
} }
val settingsItem = BrowserMenuImageText( val settingsItem = BrowserMenuImageText(
@ -152,14 +180,6 @@ class HomeMenu(
onItemTapped.invoke(Item.Settings) onItemTapped.invoke(Item.Settings)
} }
val syncedTabsItem = BrowserMenuImageText(
context.getString(R.string.library_synced_tabs),
R.drawable.ic_synced_tabs,
primaryTextColor
) {
onItemTapped.invoke(Item.SyncedTabs)
}
val helpItem = BrowserMenuImageText( val helpItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_help), context.getString(R.string.browser_menu_help),
R.drawable.ic_help, R.drawable.ic_help,
@ -211,9 +231,140 @@ class HomeMenu(
} }
} }
val desktopItem = BrowserMenuImageSwitch(
imageResource = R.drawable.ic_desktop,
label = context.getString(R.string.browser_menu_desktop_site),
initialState = { context.settings().openNextTabInDesktopMode }
) { checked ->
onItemTapped.invoke(Item.DesktopMode(checked))
}
@Suppress("ComplexMethod")
private fun newCoreMenuItems(): List<BrowserMenuItem> {
val experiments = context.components.analytics.experiments
val settings = context.components.settings
val bookmarksItem = BrowserMenuImageText(
context.getString(R.string.library_bookmarks),
R.drawable.ic_bookmark_list,
primaryTextColor
) {
onItemTapped.invoke(Item.Bookmarks)
}
// We want to validate that the Nimbus experiments library is working, from the android UI
// all the way back to the data science backend. We're not testing the user's preference
// or response, we're end-to-end testing the experiments platform.
// So here, we're running multiple identical branches with the same treatment, and if the
// user isn't targeted, then we get still get the same treatment.
// The `let` block is degenerate here, but left here so as to document the form of how experiments
// are implemented here.
val historyIcon = experiments.withExperiment(Experiments.A_A_NIMBUS_VALIDATION) {
when (it) {
ExperimentBranch.A1 -> R.drawable.ic_history
ExperimentBranch.A2 -> R.drawable.ic_history
else -> R.drawable.ic_history
}
}
val historyItem = BrowserMenuImageText(
context.getString(R.string.library_history),
historyIcon,
primaryTextColor
) {
onItemTapped.invoke(Item.History)
}
val downloadsItem = BrowserMenuImageText(
context.getString(R.string.library_downloads),
R.drawable.ic_download,
primaryTextColor
) {
onItemTapped.invoke(Item.Downloads)
}
val extensionsItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_add_ons),
R.drawable.ic_addons_extensions,
primaryTextColor
) {
onItemTapped.invoke(Item.Extensions)
}
val whatsNewItem = BrowserMenuHighlightableItem(
context.getString(R.string.browser_menu_whats_new),
R.drawable.ic_whats_new,
iconTintColorResource = primaryTextColor,
highlight = BrowserMenuHighlight.LowPriority(
notificationTint = getColor(context, R.color.whats_new_notification_color)
),
isHighlighted = { WhatsNew.shouldHighlightWhatsNew(context) }
) {
onItemTapped.invoke(Item.WhatsNew)
}
val helpItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_help),
R.drawable.ic_help,
primaryTextColor
) {
onItemTapped.invoke(Item.Help)
}
val settingsItem = BrowserMenuImageText(
context.getString(R.string.browser_menu_settings),
R.drawable.ic_settings,
primaryTextColor
) {
onItemTapped.invoke(Item.Settings)
}
// Only query account manager if it has been initialized.
// We don't want to cause its initialization just for this check.
val accountAuthItem =
if (context.components.backgroundServices.accountManagerAvailableQueue.isReady() &&
context.components.backgroundServices.accountManager.accountNeedsReauth()) {
reconnectToSyncItem
} else {
null
}
val menuItems = listOfNotNull(
bookmarksItem,
historyItem,
downloadsItem,
extensionsItem,
if (tabsTrayRewrite) syncSignInMenuItem else syncedTabsItem,
accountAuthItem,
BrowserMenuDivider(),
desktopItem,
BrowserMenuDivider(),
whatsNewItem,
helpItem,
settingsItem,
if (settings.shouldDeleteBrowsingDataOnQuit) quitItem else null
).also { items ->
items.getHighlight()?.let { onHighlightPresent(it) }
}
return menuItems
}
init { init {
val menuItems = if (FeatureFlags.toolbarMenuFeature) {
newCoreMenuItems()
} else {
oldCoreMenuItems
}
// Report initial state. // Report initial state.
onMenuBuilderChanged(BrowserMenuBuilder(coreMenuItems)) onMenuBuilderChanged(BrowserMenuBuilder(menuItems))
val menuItemsWithReconnectItem = if (FeatureFlags.toolbarMenuFeature) {
menuItems
} else {
// reconnect item is manually added to the beginning of the list
listOf(reconnectToSyncItem) + menuItems
}
// Observe account state changes, and update menu item builder with a new set of items. // Observe account state changes, and update menu item builder with a new set of items.
context.components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue { context.components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
@ -224,9 +375,11 @@ class HomeMenu(
context.components.backgroundServices.accountManager.register(object : AccountObserver { context.components.backgroundServices.accountManager.register(object : AccountObserver {
override fun onAuthenticationProblems() { override fun onAuthenticationProblems() {
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged(BrowserMenuBuilder( onMenuBuilderChanged(
listOf(reconnectToSyncItem) + coreMenuItems BrowserMenuBuilder(
)) menuItemsWithReconnectItem
)
)
} }
} }
@ -234,7 +387,7 @@ class HomeMenu(
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged( onMenuBuilderChanged(
BrowserMenuBuilder( BrowserMenuBuilder(
coreMenuItems menuItems
) )
) )
} }
@ -244,7 +397,7 @@ class HomeMenu(
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) { lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged( onMenuBuilderChanged(
BrowserMenuBuilder( BrowserMenuBuilder(
coreMenuItems menuItems
) )
) )
} }

@ -8,6 +8,7 @@ import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.lib.crash.Crash import mozilla.components.lib.crash.Crash
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
/** /**
* When the app crashes, the user has the option to report it. * When the app crashes, the user has the option to report it.
@ -26,6 +27,6 @@ class CrashReporterIntentProcessor : HomeIntentProcessor {
private fun openToCrashReporter(intent: Intent, navController: NavController) { private fun openToCrashReporter(intent: Intent, navController: NavController) {
val directions = NavGraphDirections.actionGlobalCrashReporter(intent) val directions = NavGraphDirections.actionGlobalCrashReporter(intent)
navController.navigate(directions) navController.navigateBlockingForAsyncNavGraph(directions)
} }
} }

@ -1,153 +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.home.intent
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.provider.Settings
import androidx.navigation.NavController
import mozilla.components.concept.engine.EngineSession
import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.GlobalDirections
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.SearchWidgetCreator
import org.mozilla.fenix.ext.alreadyOnDestination
/**
* Deep links in the form of `fenix://host` open different parts of the app.
*
* @param verifier [DeepLinkVerifier] that will be used to verify deep links before handling them.
*/
class DeepLinkIntentProcessor(
private val activity: HomeActivity,
private val verifier: DeepLinkVerifier
) : HomeIntentProcessor {
private val logger = Logger("DeepLinkIntentProcessor")
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
val scheme = intent.scheme?.equals(BuildConfig.DEEP_LINK_SCHEME, ignoreCase = true) ?: return false
return if (scheme) {
intent.data?.let { handleDeepLink(it, navController) }
true
} else {
false
}
}
@Suppress("ComplexMethod")
private fun handleDeepLink(deepLink: Uri, navController: NavController) {
if (!verifier.verifyDeepLink(deepLink)) {
logger.warn("Invalid deep link: $deepLink")
return
}
handleDeepLinkSideEffects(deepLink)
val globalDirections = when (deepLink.host) {
"home", "enable_private_browsing" -> GlobalDirections.Home
"urls_bookmarks" -> GlobalDirections.Bookmarks
"urls_history" -> GlobalDirections.History
"settings" -> GlobalDirections.Settings
"turn_on_sync" -> GlobalDirections.Sync
"settings_search_engine" -> GlobalDirections.SearchEngine
"settings_accessibility" -> GlobalDirections.Accessibility
"settings_delete_browsing_data" -> GlobalDirections.DeleteData
"settings_addon_manager" -> GlobalDirections.SettingsAddonManager
"settings_logins" -> GlobalDirections.SettingsLogins
"settings_tracking_protection" -> GlobalDirections.SettingsTrackingProtection
// We'd like to highlight views within the fragment
// https://github.com/mozilla-mobile/fenix/issues/11856
// The current version of UI has these features in more complex screens.
"settings_privacy" -> GlobalDirections.Settings
"home_collections" -> GlobalDirections.Home
else -> return
}
if (!navController.alreadyOnDestination(globalDirections.destinationId)) {
navController.navigate(globalDirections.navDirections)
}
}
/**
* Handle links that require more than just simple navigation.
*/
private fun handleDeepLinkSideEffects(deepLink: Uri) {
when (deepLink.host) {
"enable_private_browsing" -> {
activity.browsingModeManager.mode = BrowsingMode.Private
}
"make_default_browser" -> {
if (SDK_INT >= Build.VERSION_CODES.N) {
val settingsIntent = Intent(Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
activity.startActivity(settingsIntent)
}
}
"open" -> {
val url = deepLink.getQueryParameter("url")
if (url == null || !url.startsWith("https://")) {
logger.info("Not opening deep link: $url")
return
}
activity.openToBrowserAndLoad(
url,
newTab = true,
from = BrowserDirection.FromGlobal,
flags = EngineSession.LoadUrlFlags.external()
)
}
"settings_notifications" -> {
val intent = notificationSettings(activity)
activity.startActivity(intent)
}
"install_search_widget" -> {
if (SDK_INT >= Build.VERSION_CODES.O) {
SearchWidgetCreator.createSearchWidget(activity)
}
}
}
}
private fun notificationSettings(context: Context, channel: String? = null) =
Intent().apply {
when {
SDK_INT >= Build.VERSION_CODES.O -> {
action = channel?.let {
putExtra(Settings.EXTRA_CHANNEL_ID, it)
Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
} ?: Settings.ACTION_APP_NOTIFICATION_SETTINGS
putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName)
}
SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> {
action = "android.settings.APP_NOTIFICATION_SETTINGS"
putExtra("app_package", context.packageName)
putExtra("app_uid", context.applicationInfo.uid)
}
else -> {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addCategory(Intent.CATEGORY_DEFAULT)
data = Uri.parse("package:" + context.packageName)
}
}
}
/**
* Interface for a class that verifies deep links before they get handled.
*/
interface DeepLinkVerifier {
/**
* Verifies the given deep link and returns `true` for verified deep links or `false` for
* rejected deep links.
*/
fun verifyDeepLink(deepLink: Uri): Boolean
}
}

@ -24,6 +24,7 @@ import org.mozilla.fenix.home.sessioncontrol.viewholders.NoCollectionsMessageVie
import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.PrivateBrowsingDescriptionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TabInCollectionViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSitePagerViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.ExperimentDefaultBrowserCardViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingAutomaticSignInViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingFinishViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
@ -116,6 +117,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
val state: OnboardingState.SignedOutCanAutoSignIn val state: OnboardingState.SignedOutCanAutoSignIn
) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID) ) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID)
object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID)
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID) object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
object OnboardingTrackingProtection : object OnboardingTrackingProtection :
AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID) AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID)
@ -207,6 +210,8 @@ class SessionControlAdapter(
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder( OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view view
) )
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
else -> throw IllegalStateException() else -> throw IllegalStateException()
} }
} }

@ -39,6 +39,7 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
@ -167,6 +168,16 @@ interface SessionControlController {
* @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened] * @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
*/ */
fun handleMenuOpened() fun handleMenuOpened()
/**
* @see [ExperimentCardInteractor.onSetDefaultBrowserClicked]
*/
fun handleSetDefaultBrowser()
/**
* @see [ExperimentCardInteractor.onCloseExperimentCardClicked]
*/
fun handleCloseExperimentCard()
} }
@Suppress("TooManyFunctions", "LargeClass") @Suppress("TooManyFunctions", "LargeClass")
@ -181,6 +192,7 @@ class DefaultSessionControlController(
private val restoreUseCase: TabsUseCases.RestoreUseCase, private val restoreUseCase: TabsUseCases.RestoreUseCase,
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase, private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase, private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase,
private val fragmentStore: HomeFragmentStore, private val fragmentStore: HomeFragmentStore,
private val navController: NavController, private val navController: NavController,
private val viewLifecycleScope: CoroutineScope, private val viewLifecycleScope: CoroutineScope,
@ -403,6 +415,10 @@ class DefaultSessionControlController(
selectTab = true, selectTab = true,
startLoading = true startLoading = true
) )
if (settings.openNextTabInDesktopMode) {
activity.handleRequestDesktopMode()
}
activity.openToBrowser(BrowserDirection.FromHome) activity.openToBrowser(BrowserDirection.FromHome)
} }
@ -548,4 +564,16 @@ class DefaultSessionControlController(
) )
navController.nav(R.id.homeFragment, directions) navController.nav(R.id.homeFragment, directions)
} }
override fun handleSetDefaultBrowser() {
settings.userDismissedExperimentCard = true
metrics.track(Event.SetDefaultBrowserNewTabClicked)
activity.openSetDefaultBrowserOption()
}
override fun handleCloseExperimentCard() {
settings.userDismissedExperimentCard = true
metrics.track(Event.CloseExperimentCardClicked)
fragmentStore.dispatch(HomeFragmentAction.RemoveSetDefaultBrowserCard)
}
} }

@ -190,6 +190,18 @@ interface TopSiteInteractor {
fun onTopSiteMenuOpened() fun onTopSiteMenuOpened()
} }
interface ExperimentCardInteractor {
/**
* Called when set default browser button is clicked
*/
fun onSetDefaultBrowserClicked()
/**
* Called when close button on experiment card
*/
fun onCloseExperimentCardClicked()
}
/** /**
* Interactor for the Home screen. * Interactor for the Home screen.
* Provides implementations for the CollectionInteractor, OnboardingInteractor, * Provides implementations for the CollectionInteractor, OnboardingInteractor,
@ -199,7 +211,7 @@ interface TopSiteInteractor {
class SessionControlInteractor( class SessionControlInteractor(
private val controller: SessionControlController private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor, ) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor { TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) { override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection) controller.handleCollectionAddTabTapped(collection)
} }
@ -295,4 +307,12 @@ class SessionControlInteractor(
override fun onTopSiteMenuOpened() { override fun onTopSiteMenuOpened() {
controller.handleMenuOpened() controller.handleMenuOpened()
} }
override fun onSetDefaultBrowserClicked() {
controller.handleSetDefaultBrowser()
}
override fun onCloseExperimentCardClicked() {
controller.handleCloseExperimentCard()
}
} }

@ -12,7 +12,6 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.components.tips.Tip import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.HomeFragmentState import org.mozilla.fenix.home.HomeFragmentState
@ -22,18 +21,23 @@ import org.mozilla.fenix.home.OnboardingState
// This method got a little complex with the addition of the tab tray feature flag // This method got a little complex with the addition of the tab tray feature flag
// When we remove the tabs from the home screen this will get much simpler again. // When we remove the tabs from the home screen this will get much simpler again.
@Suppress("ComplexMethod") @Suppress("ComplexMethod", "LongParameterList")
private fun normalModeAdapterItems( private fun normalModeAdapterItems(
topSites: List<TopSite>, topSites: List<TopSite>,
collections: List<TabCollection>, collections: List<TabCollection>,
expandedCollections: Set<Long>, expandedCollections: Set<Long>,
tip: Tip?, tip: Tip?,
showCollectionsPlaceholder: Boolean showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean
): List<AdapterItem> { ): List<AdapterItem> {
val items = mutableListOf<AdapterItem>() val items = mutableListOf<AdapterItem>()
tip?.let { items.add(AdapterItem.TipItem(it)) } tip?.let { items.add(AdapterItem.TipItem(it)) }
if (showSetAsDefaultBrowserCard) {
items.add(AdapterItem.ExperimentDefaultBrowserCard)
}
if (topSites.isNotEmpty()) { if (topSites.isNotEmpty()) {
items.add(AdapterItem.TopSitePager(topSites)) items.add(AdapterItem.TopSitePager(topSites))
} }
@ -71,6 +75,13 @@ private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescri
private fun onboardingAdapterItems(onboardingState: OnboardingState): List<AdapterItem> { private fun onboardingAdapterItems(onboardingState: OnboardingState): List<AdapterItem> {
val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader) val items: MutableList<AdapterItem> = mutableListOf(AdapterItem.OnboardingHeader)
items.addAll(
listOf(
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingTrackingProtection
)
)
// Customize FxA items based on where we are with the account state: // Customize FxA items based on where we are with the account state:
items.addAll( items.addAll(
when (onboardingState) { when (onboardingState) {
@ -90,14 +101,6 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
items.addAll( items.addAll(
listOf( listOf(
AdapterItem.OnboardingSectionHeader {
val appName = it.getString(R.string.app_name)
it.getString(R.string.onboarding_feature_section_header, appName)
},
AdapterItem.OnboardingTrackingProtection,
AdapterItem.OnboardingThemePicker,
AdapterItem.OnboardingPrivateBrowsing,
AdapterItem.OnboardingToolbarPositionPicker,
AdapterItem.OnboardingPrivacyNotice, AdapterItem.OnboardingPrivacyNotice,
AdapterItem.OnboardingFinish AdapterItem.OnboardingFinish
) )
@ -112,7 +115,8 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
collections, collections,
expandedCollections, expandedCollections,
tip, tip,
showCollectionPlaceholder showCollectionPlaceholder,
showSetAsDefaultBrowserCard
) )
is Mode.Private -> privateModeAdapterItems() is Mode.Private -> privateModeAdapterItems()
is Mode.Onboarding -> onboardingAdapterItems(mode.state) is Mode.Onboarding -> onboardingAdapterItems(mode.state)

@ -0,0 +1,36 @@
/* 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.sessioncontrol.viewholders.onboarding
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.experiment_default_browser.view.*
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
class ExperimentDefaultBrowserCardViewHolder(
view: View,
private val interactor: SessionControlInteractor
) : RecyclerView.ViewHolder(view) {
init {
view.set_default_browser.setOnClickListener {
interactor.onSetDefaultBrowserClicked()
}
view.close.apply {
increaseTapArea(CLOSE_BUTTON_EXTRA_DPS)
setOnClickListener {
interactor.onCloseExperimentCardClicked()
}
}
}
companion object {
internal const val LAYOUT_ID = R.layout.experiment_default_browser
private const val CLOSE_BUTTON_EXTRA_DPS = 38
}
}

@ -10,35 +10,26 @@ import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_manual_signin.view.* import kotlinx.android.synthetic.main.onboarding_manual_signin.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
import org.mozilla.fenix.onboarding.OnboardingController
import org.mozilla.fenix.onboarding.OnboardingInteractor
class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) { class OnboardingManualSignInViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val headerText = view.header_text private val headerText = view.header_text
init { init {
val interactor = OnboardingInteractor(OnboardingController(itemView.context))
view.fxa_sign_in_button.setOnClickListener { view.fxa_sign_in_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn) it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync() val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions) Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
view.learn_more.addUnderline()
view.learn_more.setOnClickListener {
interactor.onLearnMoreClicked()
} }
} }
fun bind() { fun bind() {
val context = itemView.context val context = itemView.context
headerText.text = context.getString(R.string.onboarding_account_sign_in_header) headerText.text = context.getString(R.string.onboarding_account_sign_in_header_1)
} }
companion object { companion object {

@ -18,7 +18,7 @@ class OnboardingPrivacyNoticeViewHolder(
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
init { init {
view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_privacy_notice) view.header_text.setOnboardingIcon(R.drawable.ic_info)
val appName = view.context.getString(R.string.app_name) val appName = view.context.getString(R.string.app_name)
view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description2, appName) view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description2, appName)

@ -5,7 +5,6 @@
package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding package org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding
import android.view.View import android.view.View
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.* import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.*
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -20,29 +19,20 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
private var standardTrackingProtection: OnboardingRadioButton private var standardTrackingProtection: OnboardingRadioButton
private var strictTrackingProtection: OnboardingRadioButton private var strictTrackingProtection: OnboardingRadioButton
private var trackingProtectionToggle: SwitchCompat
init { init {
view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_tracking_protection) view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_tracking_protection)
trackingProtectionToggle = view.tracking_protection_toggle
standardTrackingProtection = view.tracking_protection_standard_option standardTrackingProtection = view.tracking_protection_standard_option
strictTrackingProtection = view.tracking_protection_strict_default strictTrackingProtection = view.tracking_protection_strict_default
view.description_text.text = view.context.getString( view.description_text.text = view.context.getString(
R.string.onboarding_tracking_protection_description_2 R.string.onboarding_tracking_protection_description_3
) )
trackingProtectionToggle.apply { val isTrackingProtectionEnabled = view.context.settings().shouldUseTrackingProtection
isChecked = view.context.settings().shouldUseTrackingProtection setupRadioGroup(isTrackingProtectionEnabled)
setOnCheckedChangeListener { _, isChecked -> updateRadioGroupState(isTrackingProtectionEnabled)
updateTrackingProtectionSetting(isChecked)
updateRadioGroupState(isChecked)
}
}
setupRadioGroup(trackingProtectionToggle.isChecked)
updateRadioGroupState(trackingProtectionToggle.isChecked)
} }
private fun setupRadioGroup(isChecked: Boolean) { private fun setupRadioGroup(isChecked: Boolean) {
@ -74,15 +64,6 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
strictTrackingProtection.isEnabled = isChecked strictTrackingProtection.isEnabled = isChecked
} }
private fun updateTrackingProtectionSetting(enabled: Boolean) {
itemView.context.settings().shouldUseTrackingProtection = enabled
with(itemView.context.components) {
val policy = core.trackingProtectionPolicyFactory.createTrackingProtectionPolicy()
useCases.settingsUseCases.updateTrackingProtection.invoke(policy)
useCases.sessionUseCases.reload.invoke()
}
}
private fun updateTrackingProtectionPolicy() { private fun updateTrackingProtectionPolicy() {
itemView.context?.components?.let { itemView.context?.components?.let {
val policy = it.core.trackingProtectionPolicyFactory val policy = it.core.trackingProtectionPolicyFactory

@ -14,6 +14,7 @@ import kotlinx.android.synthetic.main.top_site_item.*
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.feature.top.sites.TopSite.Type.DEFAULT
import mozilla.components.feature.top.sites.TopSite.Type.FRECENT import mozilla.components.feature.top.sites.TopSite.Type.FRECENT
import mozilla.components.feature.top.sites.TopSite.Type.PINNED import mozilla.components.feature.top.sites.TopSite.Type.PINNED
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -63,7 +64,7 @@ class TopSiteItemViewHolder(
fun bind(topSite: TopSite) { fun bind(topSite: TopSite) {
top_site_title.text = topSite.title top_site_title.text = topSite.title
pin_indicator.visibility = if (topSite.type == PINNED) { pin_indicator.visibility = if (topSite.type == PINNED || topSite.type == DEFAULT) {
View.VISIBLE View.VISIBLE
} else { } else {
View.GONE View.GONE
@ -79,6 +80,9 @@ class TopSiteItemViewHolder(
SupportUtils.JD_URL -> { SupportUtils.JD_URL -> {
favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_jd)) favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_jd))
} }
SupportUtils.PDD_URL -> {
favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_pdd))
}
else -> { else -> {
itemView.context.components.core.icons.loadIntoView(favicon_image, topSite.url) itemView.context.components.core.icons.loadIntoView(favicon_image, topSite.url)
} }

@ -19,34 +19,8 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.increaseTapArea import org.mozilla.fenix.ext.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView import org.mozilla.fenix.ext.loadIntoView
import org.mozilla.fenix.selection.SelectionHolder
/** import org.mozilla.fenix.selection.SelectionInteractor
* Interactor for items that can be selected on the bookmarks and history screens.
*/
interface SelectionInteractor<T> {
/**
* Called when an item is tapped to open it.
* @param item the tapped item to open.
*/
fun open(item: T)
/**
* Called when an item is long pressed and selection mode is started,
* or when selection mode has already started an an item is tapped.
* @param item the item to select.
*/
fun select(item: T)
/**
* Called when a selected item is tapped in selection mode and should no longer be selected.
* @param item the item to deselect.
*/
fun deselect(item: T)
}
interface SelectionHolder<T> {
val selectedItems: Set<T>
}
class LibrarySiteItemView @JvmOverloads constructor( class LibrarySiteItemView @JvmOverloads constructor(
context: Context, context: Context,

@ -88,7 +88,7 @@ class DefaultBookmarkController(
} }
override fun handleBookmarkEdit(node: BookmarkNode) { override fun handleBookmarkEdit(node: BookmarkNode) {
navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid)) navigateToGivenDirection(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
} }
override fun handleBookmarkSelected(node: BookmarkNode) { override fun handleBookmarkSelected(node: BookmarkNode) {
@ -118,7 +118,7 @@ class DefaultBookmarkController(
} }
override fun handleBookmarkSharing(item: BookmarkNode) { override fun handleBookmarkSharing(item: BookmarkNode) {
navigate( navigateToGivenDirection(
BookmarkFragmentDirections.actionGlobalShareFragment( BookmarkFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title)) data = arrayOf(ShareData(url = item.url, title = item.title))
) )
@ -182,7 +182,7 @@ class DefaultBookmarkController(
} }
} }
private fun navigate(directions: NavDirections) { private fun navigateToGivenDirection(directions: NavDirections) {
invokePendingDeletion.invoke() invokePendingDeletion.invoke()
navController.nav(R.id.bookmarkFragment, directions) navController.nav(R.id.bookmarkFragment, directions)
} }

@ -202,7 +202,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
true true
} }
R.id.add_bookmark_folder -> { R.id.add_bookmark_folder -> {
navigate( navigateToBookmarkFragment(
BookmarkFragmentDirections BookmarkFragmentDirections
.actionBookmarkFragmentToBookmarkAddFolderFragment() .actionBookmarkFragmentToBookmarkAddFolderFragment()
) )
@ -226,7 +226,7 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
val shareTabs = bookmarkStore.state.mode.selectedItems.map { val shareTabs = bookmarkStore.state.mode.selectedItems.map {
ShareData(url = it.url, title = it.title) ShareData(url = it.url, title = it.title)
} }
navigate( navigateToBookmarkFragment(
BookmarkFragmentDirections.actionGlobalShareFragment( BookmarkFragmentDirections.actionGlobalShareFragment(
data = shareTabs.toTypedArray() data = shareTabs.toTypedArray()
) )
@ -243,10 +243,10 @@ class BookmarkFragment : LibraryPageFragment<BookmarkNode>(), UserInteractionHan
private fun showTabTray() { private fun showTabTray() {
invokePendingDeletion() invokePendingDeletion()
navigate(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment()) navigateToBookmarkFragment(BookmarkFragmentDirections.actionGlobalTabTrayDialogFragment())
} }
private fun navigate(directions: NavDirections) { private fun navigateToBookmarkFragment(directions: NavDirections) {
invokePendingDeletion() invokePendingDeletion()
findNavController().nav( findNavController().nav(
R.id.bookmarkFragment, R.id.bookmarkFragment,

@ -9,7 +9,7 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
import org.mozilla.fenix.library.SelectionHolder import org.mozilla.fenix.selection.SelectionHolder
class BookmarkFragmentStore( class BookmarkFragmentStore(
initialState: BookmarkFragmentState initialState: BookmarkFragmentState

@ -15,8 +15,9 @@ import mozilla.components.concept.storage.BookmarkNode
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.library.LibraryPageView import org.mozilla.fenix.library.LibraryPageView
import org.mozilla.fenix.library.SelectionInteractor import org.mozilla.fenix.selection.SelectionInteractor
/** /**
* Interface for the Bookmarks view. * Interface for the Bookmarks view.
@ -119,7 +120,7 @@ class BookmarkView(
adapter = bookmarkAdapter adapter = bookmarkAdapter
} }
view.bookmark_folders_sign_in.setOnClickListener { view.bookmark_folders_sign_in.setOnClickListener {
navController.navigate(NavGraphDirections.actionGlobalTurnOnSync()) navController.navigateBlockingForAsyncNavGraph(NavGraphDirections.actionGlobalTurnOnSync())
} }
view.swipe_refresh.setOnRefreshListener { view.swipe_refresh.setOnRefreshListener {
interactor.onRequestSync() interactor.onRequestSync()

@ -8,7 +8,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.mozilla.fenix.library.SelectionHolder import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder import org.mozilla.fenix.library.downloads.viewholders.DownloadsListItemViewHolder
class DownloadAdapter( class DownloadAdapter(

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

Loading…
Cancel
Save