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
# Secrets files, e.g. tokens
.leanplum_token
.adjust_token
.sentry_token
.mls_token

@ -255,7 +255,7 @@ android {
// instead. :)
maxParallelForks = 2
forkEvery = 80
maxHeapSize = "2048m"
maxHeapSize = "3072m"
minHeapSize = "1024m"
}
}
@ -362,25 +362,6 @@ android.applicationVariants.all { variant ->
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
// -------------------------------------------------------------------------------------------------
@ -483,7 +464,6 @@ dependencies {
implementation Deps.sentry
implementation Deps.mozilla_concept_base
implementation Deps.mozilla_concept_engine
implementation Deps.mozilla_concept_menu

@ -2,7 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
---
$schema: moz://mozilla.org/schemas/glean/metrics/1-0-0
$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
no_lint:
- CATEGORY_GENERIC
@ -11,6 +11,12 @@ events:
app_opened_all_startup:
type: event
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
encompasses the home screen, browser screen, settings screen,
collections and other screens in the nav_graph.
@ -217,6 +223,32 @@ events:
notification_emails:
- fenix-core@mozilla.com
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:
type: counter
description: |
@ -555,6 +587,20 @@ search_shortcuts:
- fenix-core@mozilla.com
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:
changed_position:
type: event
@ -1187,6 +1233,40 @@ metrics:
notification_emails:
- fenix-core@mozilla.com
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:
show_search_suggestions:
@ -2433,6 +2513,19 @@ tabs_tray:
notification_emails:
- fenix-core@mozilla.com
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:
type: event
description: |
@ -3938,6 +4031,23 @@ addons:
notification_emails:
- fenix-core@mozilla.com
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:
type: boolean
description: |
@ -4126,6 +4236,95 @@ startup.timeline:
expires: "2021-08-01"
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:
type: timing_distribution
time_unit: millisecond
@ -4293,6 +4492,81 @@ perf.startup:
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
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:
history_suggestions:
@ -4312,7 +4586,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
bookmark_suggestions:
send_in_pings:
- metrics
@ -4330,7 +4604,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
search_engine_suggestions:
send_in_pings:
- metrics
@ -4348,7 +4622,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
session_suggestions:
send_in_pings:
- metrics
@ -4366,7 +4640,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
synced_tabs_suggestions:
send_in_pings:
- metrics
@ -4384,7 +4658,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
clipboard_suggestions:
send_in_pings:
- metrics
@ -4402,7 +4676,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
shortcuts_suggestions:
send_in_pings:
- metrics
@ -4420,7 +4694,7 @@ perf.awesomebar:
notification_emails:
- fenix-core@mozilla.com
- gkruglov@mozilla.com
expires: "2020-11-15"
expires: "2021-11-15"
autoplay:
visited_setting:
@ -4811,6 +5085,46 @@ engine_tab:
- fenix-core@mozilla.com
- skaspari@mozilla.com
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_suggestion_clicked:
@ -4907,6 +5221,34 @@ awesomebar:
- fenix-core@mozilla.com
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:
experiment_failure:
type: event
@ -5007,3 +5349,46 @@ android_keystore_experiment:
notification_emails:
- fenix-core@mozilla.com
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
# 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:
description: |

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

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

@ -142,8 +142,8 @@ class MenuScreenShotTest : ScreenshotTest() {
editBookmarkFolder()
Screengrab.screenshot("ThreeDotMenuBookmarksRobot_edit-bookmark-folder-menu")
// It may be needed to wait here to have the screenshot
mDevice.pressBack()
bookmarksMenu {
navigateUp()
}.openThreeDotMenu("test") {
deleteBookmarkFolder()
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.waitNotNull
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.navigationToolbar
import org.mozilla.fenix.ui.robots.settingsSubMenuLoginsAndPassword
@ -141,10 +140,7 @@ class SyncIntegrationTest {
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage) {
}.openThreeDotMenu {
verifyAddBookmarkButton()
clickAddBookmarkButton()
}
browserScreen {
}.bookmarkPage {
}.openThreeDotMenu {
}.openSettings {
}.openTurnOnSyncMenu {

@ -21,7 +21,7 @@ class GradlewBuild(object):
# Change path accordingly to go to root folder to run gradlew
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))

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

@ -88,31 +88,11 @@ class NavigationToolbarTest {
navigationToolbar {
}.openThreeDotMenu {
verifyThreeDotMenuExists()
verifyForwardButton()
}.goForward {
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
fun visitURLTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)

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

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

@ -106,7 +106,6 @@ class SettingsBasicsTest {
}
@Test
@Ignore("To be re-implemented in https://github.com/mozilla-mobile/fenix/issues/17979")
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.
val page1 = getGenericAsset(mockWebServer, 1)
@ -117,15 +116,13 @@ class SettingsBasicsTest {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(page1.url) {
}.openThreeDotMenu {
clickAddBookmarkButton()
}
}.bookmarkPage { }
navigationToolbar {
}.enterURLAndEnterToBrowser(page2.url) {
verifyUrl(page2.url.toString())
}.openThreeDotMenu {
clickAddBookmarkButton()
}
}.bookmarkPage { }
navigationToolbar {
}.enterURLAndEnterToBrowser(page3.url) {
@ -137,6 +134,7 @@ class SettingsBasicsTest {
}
}
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/19016")
@Test
fun changeThemeSetting() {
// 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
fun changeAccessibiltySettings() {
// 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 {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
}.openThreeDotMenu {
expandMenu()
}.openAddToHomeScreen {
addShortcutName(pageShortcutName)
clickAddShortcutButton()

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

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

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

@ -4,12 +4,9 @@
package org.mozilla.fenix.ui
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
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
fun tearDown() {
mockWebServer.shutdown()
}
// Verifies the list of items in the homescreen's 3 dot main menu
@Test
fun threeDotMenuItemsTest() {
if (FeatureFlags.toolbarMenuFeature) {
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}.goBackToBrowser {}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
}.closeMenu {}
homeScreen {
}.openThreeDotMenu {
verifySettingsButton()
verifyBookmarksButton()
verifyHistoryButton()
}.openSettings {
verifySettingsView()
}.goBack {
}.openThreeDotMenu {
}.goBack {}
} else {
homeScreen {
}.openThreeDotMenu {
verifySettingsButton()
verifyBookmarksButton()
verifyHistoryButton()
verifyHelpButton()
verifyWhatsNewButton()
}.openSettings {
verifySettingsView()
}.goBack {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openWhatsNew {
verifyWhatsNewURL()
}.openTabDrawer {
closeTab()
fun homeThreeDotMenuItemsTest() {
homeScreen {
}.openThreeDotMenu {
verifyBookmarksButton()
verifyHistoryButton()
verifyDownloadsButton()
verifyAddOnsButton()
if (FeatureFlags.tabsTrayRewrite) {
verifySyncSignInButton()
} else {
verifySyncedTabsButton()
}
verifyDesktopSite()
verifyWhatsNewButton()
verifyHelpButton()
verifySettingsButton()
}.openSettings {
verifySettingsView()
}.goBack {
}.openThreeDotMenu {
}.openHelp {
verifyHelpUrl()
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
}.closeMenu {
}
homeScreen {
}.openThreeDotMenu {
}.openWhatsNew {
verifyWhatsNewURL()
}.openTabDrawer {
closeTab()
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}
homeScreen {
}.openThreeDotMenu {
}.openBookmarks {
verifyBookmarksMenuView()
}.closeMenu {
}
homeScreen {
}.openThreeDotMenu {
}.openHistory {
verifyHistoryMenuView()
}
}
}

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

@ -310,8 +310,7 @@ class BrowserRobot {
// needs to wait for the right url to load before saving a bookmark
verifyUrl(url.toString())
}.openThreeDotMenu {
clickAddBookmarkButton()
}
}.bookmarkPage { }
}
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.matchers.hasItem
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.
@ -85,7 +88,6 @@ class HomeScreenRobot {
fun verifyStartSyncHeader() = assertStartSyncHeader()
fun verifyAccountsSignInButton() = assertAccountsSignInButton()
fun verifyGetToKnowHeader() = assertGetToKnowHeader()
fun verifyChooseThemeHeader() = assertChooseThemeHeader()
fun verifyChooseThemeText() = assertChooseThemeText()
fun verifyLightThemeToggle() = assertLightThemeToggle()
@ -95,18 +97,13 @@ class HomeScreenRobot {
fun verifyAutomaticThemeToggle() = assertAutomaticThemeToggle()
fun verifyAutomaticThemeDescription() = assertAutomaticThemeDescription()
fun verifyAutomaticPrivacyHeader() = assertAutomaticPrivacyHeader()
fun verifyTrackingProtectionToggle() = assertTrackingProtectionToggle()
fun verifyAutomaticPrivacyText() = assertAutomaticPrivacyText()
fun verifyAutomaticPrivacyText() = assertAlwaysPrivacyText()
// Browse privately
fun verifyBrowsePrivatelyHeader() = assertBrowsePrivatelyHeader()
fun verifyBrowsePrivatelyText() = assertBrowsePrivatelyText()
// Take a position
fun verifyTakePositionHeader() = assertTakePositionheader()
// Pick your toolbar placement
fun verifyTakePositionHeader() = assertTakePlacementHeader()
fun verifyTakePositionElements() {
assertTakePositionBottomRadioButton()
assertTakePositionTopRadioButton()
assertTakePlacementBottomRadioButton()
assertTakePacementTopRadioButton()
}
// Your privacy
@ -549,18 +546,15 @@ private fun assertWelcomeHeader() =
onView(allOf(withText("Welcome to ${appContext.appName}!")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertStartSyncHeader() =
onView(allOf(withText("Start syncing bookmarks, passwords, and more with your Firefox account.")))
private fun assertStartSyncHeader() {
scrollToElementByText(STRING_ONBOARDING_ACCOUNT_SIGN_IN_HEADER)
onView(allOf(withText(R.string.onboarding_account_sign_in_header_1)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAccountsSignInButton() =
onView(ViewMatchers.withResourceName("fxa_sign_in_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertGetToKnowHeader() =
onView(allOf(withText("Get to know ${appContext.appName}")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertChooseThemeHeader() {
scrollToElementByText("Choose your theme")
onView(withText("Choose your theme"))
@ -568,7 +562,7 @@ private fun assertChooseThemeHeader() {
}
private fun assertChooseThemeText() {
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)))
}
@ -608,40 +602,23 @@ private fun assertAutomaticThemeDescription() {
}
private fun assertAutomaticPrivacyHeader() {
scrollToElementByText("Automatic privacy")
onView(allOf(withText("Automatic privacy")))
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(allOf(withText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTrackingProtectionToggle() {
scrollToElementByText("Automatic privacy")
onView(withId(R.id.tracking_protection_toggle))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertAutomaticPrivacyText() {
scrollToElementByText("Automatic privacy")
private fun assertAlwaysPrivacyText() {
scrollToElementByText(STRING_ONBOARDING_TRACKING_PROTECTION_HEADER)
onView(
allOf(
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)))
}
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() {
scrollToElementByText("Your privacy")
onView(allOf(withText("Your privacy")))
@ -668,25 +645,25 @@ private fun assertPrivacyNoticeButton() {
private fun assertStartBrowsingButton() {
scrollToElementByText("Start browsing")
onView(allOf(withText("Start browsing")))
onView(withId(R.id.finish_button))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
// Take a position
private fun assertTakePositionheader() {
scrollToElementByText("Take a position")
onView(allOf(withText("Take a position")))
// Pick your toolbar placement
private fun assertTakePlacementHeader() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(allOf(withText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionTopRadioButton() {
scrollToElementByText("Take a position")
private fun assertTakePacementTopRadioButton() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_top_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertTakePositionBottomRadioButton() {
scrollToElementByText("Take a position")
private fun assertTakePlacementBottomRadioButton() {
scrollToElementByText(STRING_ONBOARDING_TOOLBAR_PLACEMENT_HEADER)
onView(ViewMatchers.withResourceName("toolbar_bottom_radio_button"))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}

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

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

@ -77,7 +77,7 @@ private fun assertDataCollectionOptions() {
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
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))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))

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

@ -129,12 +129,19 @@ private fun assertVideoAndAudioBlockedRecommended() = onView(withId(R.id.fourth_
private fun assertCheckAutoPayRadioButtonDefault() {
// Allow audio and video
onView(withId(R.id.block_radio))
.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))
.assertIsChecked(isChecked = false)
// Block audio and video
onView(withId(R.id.fourth_radio))
.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")))
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))))
}

@ -160,11 +160,29 @@ class TabDrawerRobot {
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 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()
@ -198,15 +216,10 @@ class TabDrawerRobot {
}
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/tab_button"))
.waitForExists(waitingTime)
mDevice.waitForIdle(waitingTime)
tabsCounter().click()
org.mozilla.fenix.ui.robots.mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/tab_layout")),
waitingTime
)
mDevice.waitNotNull(Until.findObject(By.res("$packageName:id/tab_layout")),
waitingTime)
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()

@ -6,24 +6,17 @@
package org.mozilla.fenix.ui.robots
import android.view.View
import androidx.recyclerview.widget.RecyclerView
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.click
import androidx.test.espresso.action.ViewActions.swipeDown
import androidx.test.espresso.action.ViewActions.swipeUp
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.RootMatchers
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
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.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click
@ -48,6 +40,7 @@ import org.mozilla.fenix.share.ShareFragment
/**
* Implementation of Robot Pattern for the three dot (main) menu.
*/
@Suppress("ForbiddenComment")
class ThreeDotMenuMainRobot {
fun verifyTabSettingsButton() = assertTabSettingsButton()
fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton()
@ -58,6 +51,7 @@ class ThreeDotMenuMainRobot {
fun verifyHistoryButton() = assertHistoryButton()
fun verifyBookmarksButton() = assertBookmarksButton()
fun verifySyncedTabsButton() = assertSyncedTabsButton()
fun verifySyncSignInButton() = assertSignInToSyncButton()
fun verifyHelpButton() = assertHelpButton()
fun verifyThreeDotMenuExists() = threeDotMenuRecyclerViewExists()
fun verifyForwardButton() = assertForwardButton()
@ -68,7 +62,16 @@ class ThreeDotMenuMainRobot {
fun verifyShareButton() = assertShareButton()
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
fun expandMenu() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
}
fun clickShareButton() {
var maxSwipes = 3
while (!shareButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
shareButton().click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
}
@ -85,73 +88,41 @@ class ThreeDotMenuMainRobot {
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 verifyFindInPageButton() = assertFindInPageButton()
fun verifyShareScrim() = assertShareScrim()
fun verifySendToDeviceTitle() = assertSendToDeviceTitle()
fun verifyShareALinkTitle() = assertShareALinkTitle()
fun verifyWhatsNewButton() = assertWhatsNewButton()
fun verifyAddFirefoxHome() = assertAddToFirefoxHome()
fun verifyAddToTopSitesButton() = assertAddToTopSitesButton()
fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite()
fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifyThreeDotMainMenuItems() {
if (FeatureFlags.toolbarMenuFeature) {
verifyDownloadsButton()
verifyHistoryButton()
verifyBookmarksButton()
verifySettingsButton()
verifyDesktopSite()
verifySaveCollection()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
} else {
verifyAddOnsButton()
verifyDownloadsButton()
verifyHistoryButton()
verifyBookmarksButton()
verifySyncedTabsButton()
verifySettingsButton()
verifyFindInPageButton()
verifyAddFirefoxHome()
verifyAddToMobileHome()
verifyDesktopSite()
verifySaveCollection()
verifyAddBookmarkButton()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
}
fun verifySignInToSyncButton() = assertSignInToSyncButton()
fun verifyNewTabButton() = assertNewTabButton()
fun verifyReportSiteIssueButton() = assertReportSiteIssueButton()
fun verifyPageThreeDotMainMenuItems() {
verifyNewTabButton()
verifyBookmarksButton()
verifyAddBookmarkButton()
verifyHistoryButton()
verifyDownloadsButton()
verifyAddOnsButton()
verifySignInToSyncButton()
threeDotMenuRecyclerView().perform(swipeUp())
verifyFindInPageButton()
verifyDesktopSite()
threeDotMenuRecyclerView().perform(swipeUp())
verifyReportSiteIssueButton()
verifyAddToTopSitesButton()
verifyAddToMobileHome()
verifySaveCollection()
verifySettingsButton()
verifyShareButton()
verifyForwardButton()
verifyRefreshButton()
}
private fun assertShareTabsOverlay() {
@ -166,11 +137,12 @@ class ThreeDotMenuMainRobot {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
fun openSettings(interact: SettingsRobot.() -> Unit): SettingsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
onView(allOf(withResourceName("text"), withText(R.string.browser_menu_settings)))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isCompletelyDisplayed()))
.perform(ViewActions.click())
var maxSwipes = 3
while (!settingsButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
settingsButton().click()
SettingsRobot().interact()
return SettingsRobot.Transition()
@ -185,7 +157,7 @@ class ThreeDotMenuMainRobot {
}
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)
syncedTabsButton().click()
@ -193,6 +165,15 @@ class ThreeDotMenuMainRobot {
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 {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
@ -205,7 +186,7 @@ class ThreeDotMenuMainRobot {
}
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)
historyButton().click()
@ -214,7 +195,7 @@ class ThreeDotMenuMainRobot {
}
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()
BrowserRobot().interact()
@ -222,10 +203,7 @@ class ThreeDotMenuMainRobot {
}
fun sharePage(interact: LibrarySubMenusMultipleSelectionToolbarRobot.() -> Unit): LibrarySubMenusMultipleSelectionToolbarRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Share")), waitingTime)
shareButton().click()
pressBack()
LibrarySubMenusMultipleSelectionToolbarRobot().interact()
return LibrarySubMenusMultipleSelectionToolbarRobot.Transition()
}
@ -239,7 +217,6 @@ class ThreeDotMenuMainRobot {
}
fun goForward(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Forward")), waitingTime)
forwardButton().click()
BrowserRobot().interact()
@ -273,7 +250,7 @@ class ThreeDotMenuMainRobot {
}
fun refreshPage(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.desc("Refresh")), waitingTime)
assertRefreshButton()
refreshButton().click()
BrowserRobot().interact()
@ -303,7 +280,7 @@ class ThreeDotMenuMainRobot {
}
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)
findInPageButton().click()
@ -340,6 +317,11 @@ class ThreeDotMenuMainRobot {
}
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
var maxSwipes = 3
while (!readerViewAppearanceToggle().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
readerViewAppearanceToggle().click()
ReaderViewRobot().interact()
@ -347,7 +329,7 @@ class ThreeDotMenuMainRobot {
}
fun addToFirefoxHome(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
addToFirefoxHomeButton().click()
addToTopSitesButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
@ -362,6 +344,11 @@ class ThreeDotMenuMainRobot {
}
fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
var maxSwipes = 3
while (!installPWAButton().exists() && maxSwipes != 0) {
threeDotMenuRecyclerView().perform(swipeUp())
maxSwipes--
}
installPWAButton().click()
AddToHomeScreenRobot().interact()
@ -377,6 +364,11 @@ class ThreeDotMenuMainRobot {
}
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)
saveCollectionButton().click()
ThreeDotMenuMainRobot().interact()
@ -401,18 +393,17 @@ class ThreeDotMenuMainRobot {
}
}
}
private fun threeDotMenuRecyclerView() =
onView(withId(R.id.mozac_browser_menu_recyclerView))
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 assertSettingsButton() = settingsButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(matches(isCompletelyDisplayed()))
private fun settingsButton() = mDevice.findObject(UiSelector().text("Settings"))
private fun assertSettingsButton() = assertTrue(settingsButton().waitForExists(waitingTime))
private val addOnsText = if (FeatureFlags.toolbarMenuFeature) "Extensions" else "Add-ons"
private fun addOnsButton() = onView(allOf(withText(addOnsText)))
private fun addOnsButton() = onView(allOf(withText("Add-ons")))
private fun assertAddOnsButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeDown())
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()
.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 assertHelpButton() = helpButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun forwardButton() = onView(ViewMatchers.withContentDescription("Forward"))
private fun assertForwardButton() = forwardButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun forwardButton() = mDevice.findObject(UiSelector().description("Forward"))
private fun assertForwardButton() = assertTrue(forwardButton().waitForExists(waitingTime))
private fun addBookmarkButton() = onView(ViewMatchers.withContentDescription("Bookmark"))
private fun addBookmarkButton() = onView(allOf(withId(R.id.checkbox), withText("Add")))
private fun assertAddBookmarkButton() {
onView(withId(R.id.mozac_browser_menu_menuView)).perform(swipeUp())
addBookmarkButton().check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun editBookmarkButton() = onView(ViewMatchers.withContentDescription("Edit bookmark"))
private fun editBookmarkButton() = onView(withText("Edit"))
private fun assertEditBookmarkButton() = editBookmarkButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun refreshButton() = onView(ViewMatchers.withContentDescription("Refresh"))
private fun assertRefreshButton() = refreshButton()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun refreshButton() = mDevice.findObject(UiSelector().description("Refresh"))
private fun assertRefreshButton() = assertTrue(refreshButton().waitForExists(waitingTime))
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()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun shareButton() = onView(ViewMatchers.withContentDescription("Share"))
private fun assertShareButton() = shareButton()
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun shareButton() = mDevice.findObject(UiSelector().description("Share"))
private fun assertShareButton() = assertTrue(shareButton().waitForExists(waitingTime))
private fun browserViewSaveCollectionButton() = onView(
allOf(
withText("Save to collection"),
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)
withEffectiveVisibility(Visibility.VISIBLE)
)
)
@ -492,11 +483,14 @@ private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
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 assertFindInPageButton() = findInPageButton()
private fun shareScrim() = onView(withResourceName("closeSharingScrim"))
private fun assertShareScrim() =
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 readerViewAppearanceToggle() =
onView(allOf(withText(R.string.browser_menu_read_appearance)))
private fun assertReaderViewAppearanceButton(visible: Boolean) = readerViewAppearanceToggle()
.check(
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE)) else ViewAssertions.doesNotExist()
)
mDevice.findObject(UiSelector().text("Customize reader view"))
private fun assertReaderViewAppearanceButton(visible: Boolean) {
var maxSwipes = 3
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)))
private fun assertAddToFirefoxHome() {
private fun assertAddToTopSitesButton() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
RecyclerViewActions.scrollTo<RecyclerView.ViewHolder>(
@ -545,6 +551,7 @@ private fun assertAddToFirefoxHome() {
private fun addToMobileHomeButton() =
onView(allOf(withText(R.string.browser_menu_add_to_homescreen)))
private fun assertAddToMobileHome() {
onView(withId(R.id.mozac_browser_menu_recyclerView))
.perform(
@ -554,7 +561,7 @@ private fun assertAddToMobileHome() {
).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() =
onView(allOf(withText(R.string.browser_menu_desktop_site)))
@ -602,3 +609,5 @@ private fun assertShareAllTabsButton() {
.check(
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 loginStorageDelegate = GeckoLoginStorageDelegate(storage)
@Suppress("Deprecation")
geckoRuntime.loginStorageDelegate = GeckoLoginDelegateWrapper(loginStorageDelegate)
return geckoRuntime

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

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

@ -25,6 +25,7 @@
<application
android:name=".FenixApplication"
android:allowBackup="false"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
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.ext.components
import org.mozilla.fenix.ext.isOnline
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import java.lang.ref.WeakReference
class AppRequestInterceptor(
@ -97,7 +98,7 @@ class AppRequestInterceptor(
// Navigate and trigger add-on installation.
matchResult.groupValues.getOrNull(1)?.let { addonId ->
navController?.get()?.navigate(
navController?.get()?.navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalAddonsManagementFragment(addonId)
)

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

@ -37,15 +37,10 @@ object FeatureFlags {
/**
* 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.
*/
val tabsTrayRewrite = Config.channel.isDebug
/**
* Enables the updated icon set look and feel.
*/
val newIconSet = Config.channel.isNightlyOrDebug
const val tabsTrayRewrite = true
}

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

@ -16,6 +16,7 @@ import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ActionMode
import android.view.ViewConfiguration
import android.view.WindowManager.LayoutParams.FLAG_SECURE
import androidx.annotation.CallSuper
@ -32,13 +33,14 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.NavigationUI
import kotlinx.android.synthetic.main.activity_home.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers.IO
import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
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.breadcrumb
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.measureNoInline
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.home.HomeFragmentDirections
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.OpenSpecificTabIntentProcessor
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.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.NavGraphProvider
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.ProfilerMarkers
import org.mozilla.fenix.perf.StartupPathProvider
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StartupTypeTelemetry
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService
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.share.AddNewDeviceFragmentDirections
import org.mozilla.fenix.sync.SyncedTabsFragmentDirections
import org.mozilla.fenix.tabstray.TabsTrayFragmentDirections
import org.mozilla.fenix.tabtray.TabTrayDialogFragment
import org.mozilla.fenix.tabtray.TabTrayDialogFragmentDirections
import org.mozilla.fenix.theme.DefaultThemeManager
@ -121,7 +127,7 @@ import java.lang.ref.WeakReference
* - browser screen
*/
@OptIn(ExperimentalCoroutinesApi::class)
@SuppressWarnings("TooManyFunctions", "LargeClass")
@SuppressWarnings("TooManyFunctions", "LargeClass", "LongParameterList")
open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// DO NOT MOVE ANYTHING ABOVE THIS, GETTING INIT TIME IS CRITICAL
// 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.
protected val homeActivityInitTimeStampNanoSeconds = SystemClock.elapsedRealtimeNanos()
private var webExtScope: CoroutineScope? = null
lateinit var themeManager: ThemeManager
lateinit var browsingModeManager: BrowsingModeManager
@ -154,7 +159,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
listOf(
SpeechProcessingIntentProcessor(this, components.core.store, components.analytics.metrics),
StartSearchIntentProcessor(components.analytics.metrics),
DeepLinkIntentProcessor(this, components.analytics.leanplumMetricsService),
OpenBrowserIntentProcessor(this, ::getIntentSessionId),
OpenSpecificTabIntentProcessor(this)
)
@ -165,6 +169,12 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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 {
// DO NOT MOVE ANYTHING ABOVE THIS addMarker CALL.
components.core.engine.profiler?.addMarker("Activity.onCreate", "HomeActivity")
@ -172,6 +182,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.strictMode.attachListenerToDisablePenaltyDeath(supportFragmentManager)
// There is disk read violations on some devices such as samsung and pixel for android 9/10
components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
// Theme setup should always be called before super.onCreate
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
super.onCreate(savedInstanceState)
}
@ -187,8 +199,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
components.publicSuffixList.prefetch()
setupThemeAndBrowsingMode(getModeFromIntentOrLastKnown(intent))
setContentView(R.layout.activity_home)
setContentView(R.layout.activity_home).run {
// 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
if (isVisuallyComplete) {
@ -204,17 +219,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
it.start()
}
if (isActivityColdStarted(
intent,
savedInstanceState
) && !externalSourceIntentProcessors.any {
it.process(
intent,
navHost.navController,
this.intent
)
}
) {
if (isActivityColdStarted(intent, savedInstanceState) &&
!externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) }) {
navigateToBrowserOnColdStart()
}
@ -252,6 +258,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
captureSnapshotTelemetryMetrics()
startupTelemetryOnCreateCalled(intent.toSafeIntent(), savedInstanceState != null)
startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnHomeActivityOnCreate(lifecycle)
}
components.core.requestInterceptor.setNavigationController(navHost.navController)
@ -262,11 +272,19 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
safeIntent: SafeIntent,
hasSavedInstanceState: Boolean
) {
// This function gets overridden by subclasses.
components.appStartupTelemetry.onHomeActivityOnCreate(
safeIntent,
hasSavedInstanceState,
homeActivityInitTimeStampNanoSeconds, rootContainer
)
components.performance.coldStartupDurationTelemetry.onHomeActivityOnCreate(
components.performance.visualCompletenessQueue,
components.startupStateProvider,
safeIntent,
rootContainer
)
}
override fun onRestart() {
@ -455,6 +473,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
intent?.let {
handleNewIntent(it)
}
startupPathProvider.onIntentReceived(intent)
}
open fun handleNewIntent(intent: Intent) {
@ -519,6 +538,20 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
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")
// Defining the positions as constants doesn't seem super useful here.
private fun actionSorter(actions: Array<String>): Array<String> {
@ -721,10 +754,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
customTabSessionId: String? = null,
engine: SearchEngine? = null,
forceSearch: Boolean = false,
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none()
flags: EngineSession.LoadUrlFlags = EngineSession.LoadUrlFlags.none(),
requestDesktopMode: Boolean = false
) {
openToBrowser(from, customTabSessionId)
load(searchTermOrURL, newTab, engine, forceSearch, flags)
load(searchTermOrURL, newTab, engine, forceSearch, flags, requestDesktopMode)
}
fun openToBrowser(from: BrowserDirection, customTabSessionId: String? = null) {
@ -774,8 +808,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
AddonPermissionsDetailsFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromLoginDetailFragment ->
LoginDetailFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
BrowserDirection.FromTabTrayDialog ->
TabTrayDialogFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromTabTray ->
TabsTrayFragmentDirections.actionGlobalBrowser(customTabSessionId)
BrowserDirection.FromRecentlyClosed ->
RecentlyClosedFragmentDirections.actionGlobalBrowser(customTabSessionId)
}
@ -790,7 +826,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
newTab: Boolean,
engine: SearchEngine?,
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 mode = browsingModeManager.mode
@ -807,6 +844,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
// and let it try to load whatever was entered.
if ((!forceSearch && searchTermOrURL.isUrl()) || engine == null) {
loadUrlUseCase.invoke(searchTermOrURL.toNormalizedUrl(), flags)
if (requestDesktopMode) {
handleRequestDesktopMode()
}
} else {
if (newTab) {
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() {
// 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
@ -850,7 +904,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
}
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) {
inflater = PerformanceInflater(LayoutInflater.from(baseContext), this)
}
@ -885,7 +943,7 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
webExtensionId = webExtensionState.id,
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)
addonProgressOverlay?.visibility = View.GONE
showInstallationDialog(it)
Addons.hasInstalledAddons.set(true)
}
},
onError = { _, e ->

@ -8,6 +8,7 @@ import androidx.navigation.NavController
import mozilla.components.feature.addons.Addon
import mozilla.components.feature.addons.ui.AddonsManagerAdapterDelegate
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.navigateSafe
/**
@ -55,6 +56,6 @@ class AddonsManagementView(
AddonsManagementFragmentDirections.actionAddonsManagementFragmentToNotYetSupportedAddonFragment(
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.AddonManagerException
import mozilla.components.feature.addons.ui.translateName
import org.mozilla.fenix.GleanMetrics.Addons
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
@ -128,7 +129,6 @@ class InstalledAddonDetailsFragment : Fragment() {
)
)
}
Addons.hasEnabledAddons.set(true)
}
},
onError = {
@ -196,6 +196,9 @@ class InstalledAddonDetailsFragment : Fragment() {
view.settings.apply {
isVisible = shouldSettingsBeVisible()
setOnClickListener {
requireContext().components.analytics.metrics.track(
Event.AddonOpenSetting(addon.id)
)
val settingUrl = addon.installedState?.optionsPageUrl ?: return@setOnClickListener
val directions = if (addon.installedState?.openOptionsPageInTab == true) {
val components = it.context.components
@ -213,7 +216,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections
.actionInstalledAddonFragmentToAddonInternalSettingsFragment(addon)
}
Navigation.findNavController(this).navigate(directions)
Navigation.findNavController(this).navigateBlockingForAsyncNavGraph(directions)
}
}
}
@ -224,7 +227,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonDetailsFragment(
addon
)
Navigation.findNavController(view).navigate(directions)
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
}
@ -234,7 +237,7 @@ class InstalledAddonDetailsFragment : Fragment() {
InstalledAddonDetailsFragmentDirections.actionInstalledAddonFragmentToAddonPermissionsDetailsFragment(
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.UserInteractionHandler
import mozilla.components.support.base.feature.ViewBoundFeatureWrapper
import mozilla.components.support.ktx.android.view.exitImmersiveModeIfNeeded
import mozilla.components.support.ktx.android.view.hideKeyboard
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
@ -127,9 +128,9 @@ import java.lang.ref.WeakReference
import mozilla.components.feature.session.behavior.EngineViewBrowserToolbarBehavior
import mozilla.components.feature.webauthn.WebAuthnFeature
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.ext.enterToImmersiveMode
import org.mozilla.fenix.ext.exitImmersiveModeIfNeeded
import org.mozilla.fenix.ext.measureNoInline
import mozilla.components.feature.session.behavior.ToolbarPosition as MozacToolbarPosition
@ -282,7 +283,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
val readerMenuController = DefaultReaderModeController(
readerViewFeature,
view.readerViewControlsBar,
isPrivate = activity.browsingModeManager.mode.isPrivate
isPrivate = activity.browsingModeManager.mode.isPrivate,
onReaderModeChanged = { activity.finishActionMode() }
)
val browserToolbarController = DefaultBrowserToolbarController(
store = store,
@ -370,8 +372,12 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
store = store,
sessionId = customTabSessionId,
stub = view.stubFindInPage,
engineView = view.engineView,
toolbar = browserToolbarView.view
engineView = engineView,
toolbarInfo = FindInPageIntegration.ToolbarInfo(
browserToolbarView.view,
!context.settings().shouldUseFixedTopToolbar && context.settings().isDynamicToolbarEnabled,
!context.settings().shouldUseBottomToolbar
)
),
owner = this,
view = view
@ -552,7 +558,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
showPage = true,
sessionId = getCurrentTab()?.id
)
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
},
onNeedToRequestPermissions = { permissions ->
@ -563,7 +569,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
browserAnimator.captureEngineViewAndDrawStatically {
val directions =
NavGraphDirections.actionGlobalSavedLoginsAuthFragment()
findNavController().navigate(directions)
findNavController().navigateBlockingForAsyncNavGraph(directions)
}
}
),
@ -851,12 +857,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
view: View
): List<ContextMenuCandidate>
@CallSuper
override fun onStart() {
super.onStart()
sitePermissionWifiIntegration.get()?.maybeAddWifiConnectedListener()
}
@VisibleForTesting
internal fun observeRestoreComplete(store: BrowserStore, navController: NavController) {
val activity = activity as HomeActivity
@ -980,7 +980,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
}
override fun onBackLongPressed(): Boolean {
findNavController().navigate(
findNavController().navigateBlockingForAsyncNavGraph(
NavGraphDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)

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

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

@ -24,9 +24,11 @@ interface ReaderModeController {
class DefaultReaderModeController(
private val readerViewFeature: ViewBoundFeatureWrapper<ReaderViewFeature>,
private val readerViewControlsBar: View,
private val isPrivate: Boolean = false
private val isPrivate: Boolean = false,
private val onReaderModeChanged: () -> Unit = {}
) : ReaderModeController {
override fun hideReaderView() {
onReaderModeChanged()
readerViewFeature.withFeature {
it.hideReaderView()
it.hideControls()
@ -34,6 +36,7 @@ class DefaultReaderModeController(
}
override fun showReaderView() {
onReaderModeChanged()
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
* 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.ViewGroup
@ -14,10 +14,14 @@ import androidx.recyclerview.widget.RecyclerView
import mozilla.components.support.ktx.android.view.putCompoundDrawablesRelativeWithIntrinsicBounds
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 onNewCollectionClicked: () -> Unit
) : RecyclerView.Adapter<CollectionsAdapter.CollectionItemViewHolder>() {
) : RecyclerView.Adapter<CollectionsListAdapter.CollectionItemViewHolder>() {
@VisibleForTesting
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.components.metrics.AdjustMetricsService
import org.mozilla.fenix.components.metrics.GleanMetricsService
import org.mozilla.fenix.components.metrics.LeanplumMetricsService
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.components
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 {
MetricController.create(
listOf(
GleanMetricsService(context, lazy { context.components.core.store }),
leanplumMetricsService,
AdjustMetricsService(context as Application)
),
isDataTelemetryEnabled = { context.settings().isTelemetryEnabled },

@ -216,14 +216,9 @@ internal class TelemetryAccountObserver(
}?.let {
metricController.track(it)
}
// Used by Leanplum as a context variable.
settings.fxaSignedIn = true
}
override fun onLoggedOut() {
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.R
import org.mozilla.fenix.autofill.AutofillUnlockActivity
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components
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.utils.ClipboardHandler
import org.mozilla.fenix.utils.Mockable
@ -68,7 +71,8 @@ class Components(private val context: Context) {
core.sessionManager,
core.store,
core.webAppShortcutManager,
core.topSitesStorage
core.topSitesStorage,
core.bookmarksStorage
)
}
@ -173,4 +177,8 @@ class Components(private val context: Context) {
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.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.prompts.PromptMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
import mozilla.components.feature.readerview.ReaderViewMiddleware
@ -64,7 +65,6 @@ import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService
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.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.telemetry.TelemetryMiddleware
import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay
@ -194,7 +195,8 @@ class Core(
additionalBundledSearchEngineIds = listOf("reddit", "youtube"),
migration = SearchMigration(context)
),
RecordingDevicesMiddleware(context)
RecordingDevicesMiddleware(context),
PromptMiddleware()
)
BrowserStore(
@ -335,6 +337,13 @@ class Core(
SupportUtils.JD_URL
)
)
defaultTopSites.add(
Pair(
context.getString(R.string.default_top_site_pdd),
SupportUtils.PDD_URL
)
)
} else {
defaultTopSites.add(
Pair(

@ -5,7 +5,10 @@
package org.mozilla.fenix.components
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
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.store.BrowserStore
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.view.FindInPageView
import mozilla.components.support.base.feature.LifecycleAwareFeature
import org.mozilla.fenix.components.FindInPageIntegration.ToolbarInfo
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
class FindInPageIntegration(
private val store: BrowserStore,
private val sessionId: String? = null,
stub: ViewStub,
private val engineView: EngineView,
private val toolbar: BrowserToolbar
private val toolbarInfo: ToolbarInfo
) : InflationAwareFeature(stub) {
override fun onViewInflated(view: View): LifecycleAwareFeature {
return FindInPageFeature(store, view as FindInPageView, engineView) {
toolbar.visibility = View.VISIBLE
restorePreviousLayout()
view.visibility = View.GONE
}
}
override fun onLaunch(view: View, feature: LifecycleAwareFeature) {
store.state.findCustomTabOrSelectedTab(sessionId)?.let { tab ->
// Always hide the toolbar and display find in page query
toolbar.visibility = View.GONE
prepareLayoutForFindBar()
view.visibility = View.VISIBLE
(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 mozilla.components.service.fxa.ServerConfig
import mozilla.components.service.fxa.ServerConfig.Server
import org.mozilla.fenix.Config
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"
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 tokenServerOverride = context.settings().overrideSyncTokenServer.ifEmpty { null }
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)
}

@ -5,6 +5,7 @@
package org.mozilla.fenix.components
import mozilla.components.support.utils.RunWhenReadyQueue
import org.mozilla.fenix.perf.ColdStartupDurationTelemetry
import org.mozilla.fenix.perf.VisualCompletenessQueue
import org.mozilla.fenix.perf.lazyMonitored
@ -13,4 +14,5 @@ import org.mozilla.fenix.perf.lazyMonitored
*/
class PerformanceComponent {
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.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.BookmarksStorage
import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.feature.contextmenu.ContextMenuUseCases
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.TopSitesUseCases
import mozilla.components.support.locale.LocaleUseCases
import org.mozilla.fenix.components.bookmarks.BookmarksUseCase
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.Mockable
@ -38,7 +40,8 @@ class UseCases(
private val sessionManager: SessionManager,
private val store: BrowserStore,
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.
@ -94,4 +97,9 @@ class UseCases(
* Use cases that handle locale management.
*/
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]
* The basic idea is to collect these metrics from different phases of startup through
* [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")
class AppStartupTelemetry(

@ -182,6 +182,7 @@ sealed class Event {
object ClosedExistingTab : Event()
object TabsTrayPrivateModeTapped : Event()
object TabsTrayNormalModeTapped : Event()
object TabsTraySyncedModeTapped : Event()
object NewTabTapped : Event()
object NewPrivateTabTapped : Event()
object TabsTrayMenuOpened : Event()
@ -224,6 +225,17 @@ sealed class Event {
object SearchSuggestionClicked : 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
data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -325,6 +337,11 @@ sealed class Event {
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() {
override val extras: Map<Tip.displayedKeys, String>?
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,
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,
BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS
BOOKMARKS, HISTORY, SYNC_TABS, DOWNLOADS, SET_DEFAULT_BROWSER, SYNC_ACCOUNT
}
override val extras: Map<Events.browserMenuActionKeys, String>?
@ -597,7 +614,7 @@ sealed class Event {
data class AutoPlaySettingChanged(val setting: AutoplaySetting) : Event() {
enum class AutoplaySetting {
BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL
BLOCK_CELLULAR, BLOCK_AUDIO, BLOCK_ALL, ALLOW_ALL
}
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.ErrorPage
import org.mozilla.fenix.GleanMetrics.Events
import org.mozilla.fenix.GleanMetrics.ExperimentsDefaultBrowser
import org.mozilla.fenix.GleanMetrics.FindInPage
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.Logins
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.SearchSuggestions
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.SyncAuth
import org.mozilla.fenix.GleanMetrics.SyncedTabs
@ -192,6 +197,15 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.browserMenuAction.record(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>(
{ BookmarksManagement.open.record(it) }
)
@ -586,6 +600,10 @@ private val Event.wrapper: EventWrapper<*>?
{ Addons.openAddonInToolbarMenu.record(it) },
{ Addons.openAddonInToolbarMenuKeys.valueOf(it) }
)
is Event.AddonOpenSetting -> EventWrapper(
{ Addons.openAddonSetting.record(it) },
{ Addons.openAddonSettingKeys.valueOf(it) }
)
is Event.TipDisplayed -> EventWrapper(
{ Tip.displayed.record(it) },
{ Tip.displayedKeys.valueOf(it) }
@ -667,6 +685,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TabsTrayNormalModeTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.normalModeTapped.record(it) }
)
is Event.TabsTraySyncedModeTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.syncedModeTapped.record(it) }
)
is Event.NewTabTapped -> EventWrapper<NoExtraKeys>(
{ TabsTray.newTabTapped.record(it) }
)
@ -805,6 +826,22 @@ private val Event.wrapper: EventWrapper<*>?
is Event.SecurePrefsReset -> EventWrapper<NoExtraKeys>(
{ 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:
is Event.AddBookmark -> null
@ -815,7 +852,6 @@ private val Event.wrapper: EventWrapper<*>?
is Event.FennecToFenixMigrated -> null
is Event.AddonInstalled -> null
is Event.SearchWidgetInstalled -> null
is Event.ChangedToDefaultBrowser -> 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
import androidx.annotation.VisibleForTesting
import com.leanplum.Leanplum
import mozilla.components.browser.awesomebar.facts.BrowserAwesomeBarFacts
import mozilla.components.browser.menu.facts.BrowserMenuFacts
import mozilla.components.browser.toolbar.facts.ToolbarFacts
@ -179,7 +178,7 @@ internal class ReleaseMetricController(
}
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 -> {
metadata?.get("id")?.let { Event.AddonsOpenInToolbarMenu(it.toString()) }
@ -218,7 +217,6 @@ internal class ReleaseMetricController(
if (installedAddons is List<*>) {
settings.installedAddonsCount = installedAddons.size
settings.installedAddonsList = installedAddons.joinToString(",")
Leanplum.setUserAttributes(mapOf("installed_addons" to installedAddons.size))
}
}
@ -226,7 +224,6 @@ internal class ReleaseMetricController(
if (enabledAddons is List<*>) {
settings.enabledAddonsCount = enabledAddons.size
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.MetricController
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.settings
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
if (store.state.getNormalOrPrivateTabs(it.content.private).count() == 1) {
homeViewModel.sessionToDelete = it.id
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome()
)
} else {
@ -132,7 +133,7 @@ class DefaultBrowserToolbarController(
Event.TabCounterMenuItemTapped(Event.TabCounterMenuItemTapped.Item.NEW_TAB)
)
activity.browsingModeManager.mode = BrowsingMode.Normal
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
@ -143,7 +144,7 @@ class DefaultBrowserToolbarController(
)
)
activity.browsingModeManager.mode = BrowsingMode.Private
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
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.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.navigateSafe
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.settings.deletebrowsingdata.deleteAndQuit
import org.mozilla.fenix.utils.Do
import org.mozilla.fenix.utils.Settings
@ -91,7 +93,7 @@ class DefaultBrowserToolbarMenuController(
Do exhaustive when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.InstallToHomeScreen -> {
is ToolbarMenu.Item.InstallPwaToHomeScreen -> {
settings.installPwaOpened = true
MainScope().launch {
with(activity.components.useCases.webAppUseCases) {
@ -128,6 +130,18 @@ class DefaultBrowserToolbarMenuController(
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 -> {
// 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
@ -147,22 +161,9 @@ class DefaultBrowserToolbarMenuController(
readerModeController.showControls()
metrics.track(Event.ReaderModeAppearanceOpened)
}
is ToolbarMenu.Item.OpenInApp -> {
settings.openInAppOpened = true
val appLinksUseCases = activity.components.useCases.appLinksUseCases
val getRedirect = appLinksUseCases.appLinkRedirect
currentSession?.let {
val redirect = getRedirect.invoke(it.content.url)
redirect.appIntent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
appLinksUseCases.openAppLink.invoke(redirect.appIntent)
}
}
// todo === End ===
is ToolbarMenu.Item.Back -> {
if (item.viewHistory) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)
@ -175,7 +176,7 @@ class DefaultBrowserToolbarMenuController(
}
is ToolbarMenu.Item.Forward -> {
if (item.viewHistory) {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalTabHistoryDialogFragment(
activeSessionId = customTabSessionId
)
@ -212,7 +213,7 @@ class DefaultBrowserToolbarMenuController(
),
showPage = true
)
navController.navigate(directions)
navController.navigateBlockingForAsyncNavGraph(directions)
}
is ToolbarMenu.Item.Settings -> browserAnimator.captureEngineViewAndDrawStatically {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment()
@ -224,6 +225,19 @@ class DefaultBrowserToolbarMenuController(
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 -> {
currentSession?.let {
sessionUseCases.requestDesktopSite.invoke(
@ -335,10 +349,14 @@ class DefaultBrowserToolbarMenuController(
)
}
is ToolbarMenu.Item.NewTab -> {
navController.navigate(
navController.navigateBlockingForAsyncNavGraph(
BrowserFragmentDirections.actionGlobalHome(focusOnAddressBar = true)
)
}
is ToolbarMenu.Item.SetDefaultBrowser -> {
metrics.track(Event.SetDefaultBrowserToolbarMenuClicked)
activity.openSetDefaultBrowserOption()
}
}
}
@ -356,15 +374,12 @@ class DefaultBrowserToolbarMenuController(
@Suppress("ComplexMethod")
private fun trackToolbarItemInteraction(item: ToolbarMenu.Item) {
val eventItem = when (item) {
// TODO: These can be removed for https://github.com/mozilla-mobile/fenix/issues/17870
// todo === Start ===
is ToolbarMenu.Item.OpenInFenix -> Event.BrowserMenuItemTapped.Item.OPEN_IN_FENIX
is ToolbarMenu.Item.InstallToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.InstallPwaToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.Quit -> Event.BrowserMenuItemTapped.Item.QUIT
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
is ToolbarMenu.Item.CustomizeReaderView ->
Event.BrowserMenuItemTapped.Item.READER_MODE_APPEARANCE
is ToolbarMenu.Item.OpenInApp -> Event.BrowserMenuItemTapped.Item.OPEN_IN_APP
// todo === End ===
is ToolbarMenu.Item.Back -> Event.BrowserMenuItemTapped.Item.BACK
is ToolbarMenu.Item.Forward -> Event.BrowserMenuItemTapped.Item.FORWARD
is ToolbarMenu.Item.Reload -> Event.BrowserMenuItemTapped.Item.RELOAD
@ -382,12 +397,14 @@ class DefaultBrowserToolbarMenuController(
is ToolbarMenu.Item.AddToTopSites -> Event.BrowserMenuItemTapped.Item.ADD_TO_TOP_SITES
is ToolbarMenu.Item.AddToHomeScreen -> Event.BrowserMenuItemTapped.Item.ADD_TO_HOMESCREEN
is ToolbarMenu.Item.SyncedTabs -> Event.BrowserMenuItemTapped.Item.SYNC_TABS
is ToolbarMenu.Item.SyncAccount -> Event.BrowserMenuItemTapped.Item.SYNC_ACCOUNT
is ToolbarMenu.Item.Bookmark -> Event.BrowserMenuItemTapped.Item.BOOKMARK
is ToolbarMenu.Item.AddonsManager -> Event.BrowserMenuItemTapped.Item.ADDONS_MANAGER
is ToolbarMenu.Item.Bookmarks -> Event.BrowserMenuItemTapped.Item.BOOKMARKS
is ToolbarMenu.Item.History -> Event.BrowserMenuItemTapped.Item.HISTORY
is ToolbarMenu.Item.Downloads -> Event.BrowserMenuItemTapped.Item.DOWNLOADS
is ToolbarMenu.Item.NewTab -> Event.BrowserMenuItemTapped.Item.NEW_TAB
is ToolbarMenu.Item.SetDefaultBrowser -> Event.BrowserMenuItemTapped.Item.SET_DEFAULT_BROWSER
}
metrics.track(Event.BrowserMenuItemTapped(eventItem))

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

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

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

@ -50,7 +50,8 @@ class DynamicDownloadDialogBehavior<V : View>(
/**
* 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,
@ -64,7 +65,9 @@ class DynamicDownloadDialogBehavior<V : View>(
*/
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal val shouldScroll: Boolean
get() = engineView?.getInputResult() == EngineView.InputResult.INPUT_RESULT_HANDLED
get() = engineView?.getInputResultDetail()?.let {
(it.canScrollToBottom() || it.canScrollToTop())
} ?: false
override fun onStartNestedScroll(
coordinatorLayout: CoordinatorLayout,
@ -78,7 +81,7 @@ class DynamicDownloadDialogBehavior<V : View>(
shouldSnapAfterScroll = type == ViewCompat.TYPE_TOUCH
snapAnimator.cancel()
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
// state where they cannot show it
forceExpand(child)

@ -7,8 +7,8 @@ package org.mozilla.fenix.experiments
class Experiments {
companion object {
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 DEFAULT_BROWSER = "fenix-default-browser"
}
}
@ -18,5 +18,8 @@ class ExperimentBranch {
const val CONTROL = "control"
const val A1 = "a1"
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.WindowManager
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.
@ -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
* 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() {
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
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
@ -24,19 +41,6 @@ fun Activity.enterToImmersiveMode() {
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(
message: String,
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
* 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
import androidx.annotation.IdRes
@ -10,12 +13,15 @@ import androidx.navigation.NavDirections
import androidx.navigation.NavOptions
import io.sentry.Sentry
import org.mozilla.fenix.components.isSentryEnabled
import org.mozilla.fenix.perf.NavGraphProvider
/**
* Navigate from the fragment with [id] using the given [directions].
* If the id doesn't match the current destination, an error is recorded.
*/
fun NavController.nav(@IdRes id: Int?, directions: NavDirections, navOptions: NavOptions? = null) {
NavGraphProvider.blockForNavGraphInflation(this)
if (id == null || this.currentDestination?.id == id) {
this.navigate(directions, navOptions)
} 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 {
return destId?.let { currentDestination?.id == it || popBackStack(it, false) } ?: false
}
@ -38,6 +59,6 @@ fun NavController.navigateSafe(
directions: NavDirections
) {
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.BitmapFactory
import android.net.Uri
import android.text.Editable
import android.util.Patterns
import android.webkit.URLUtil
import androidx.core.net.toUri
@ -114,6 +115,11 @@ fun String.simplifiedUrl(): String {
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) {
// 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').

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

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

@ -13,15 +13,20 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuHighlight
import mozilla.components.browser.menu.BrowserMenuItem
import mozilla.components.browser.menu.ext.getHighlight
import mozilla.components.browser.menu.item.BrowserMenuDivider
import mozilla.components.browser.menu.item.BrowserMenuHighlightableItem
import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount
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.components.accounts.FenixAccountManager
import org.mozilla.fenix.experiments.ExperimentBranch
import org.mozilla.fenix.experiments.Experiments
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.whatsnew.WhatsNew
@Suppress("LargeClass", "LongMethod")
class HomeMenu(
private val lifecycleOwner: LifecycleOwner,
private val context: Context,
@ -38,26 +44,28 @@ class HomeMenu(
private val onHighlightPresent: (BrowserMenuHighlight) -> Unit = {}
) {
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 Help : Item()
object AddonsManager : Item()
object Settings : Item()
object SyncedTabs : Item()
object History : Item()
object Bookmarks : Item()
object Downloads : Item()
object Quit : Item()
object Sync : Item()
object ReconnectSync : Item()
data class DesktopMode(val checked: Boolean) : Item()
}
private val primaryTextColor =
ThemeManager.resolveAttribute(R.attr.primaryText, context)
private val syncDisconnectedColor = ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
private val syncDisconnectedBackgroundColor = context.getColorFromAttr(R.attr.syncDisconnectedBackground)
private val primaryTextColor = ThemeManager.resolveAttribute(R.attr.primaryText, context)
private val syncDisconnectedColor =
ThemeManager.resolveAttribute(R.attr.syncDisconnected, context)
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 accountManager = FenixAccountManager(context)
// 'Reconnect' and 'Quit' items aren't needed most of the time, so we'll only create the if necessary.
private val reconnectToSyncItem by lazy {
@ -72,7 +80,7 @@ class HomeMenu(
),
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(
context.getString(R.string.browser_menu_whats_new),
R.drawable.ic_whats_new,
@ -98,17 +133,10 @@ class HomeMenu(
) {
onItemTapped.invoke(Item.WhatsNew)
}
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(
context.getString(R.string.library_bookmarks),
bookmarksIcon,
R.drawable.ic_bookmark_list,
primaryTextColor
) {
onItemTapped.invoke(Item.Bookmarks)
@ -141,7 +169,7 @@ class HomeMenu(
R.drawable.ic_addons_extensions,
primaryTextColor
) {
onItemTapped.invoke(Item.AddonsManager)
onItemTapped.invoke(Item.Extensions)
}
val settingsItem = BrowserMenuImageText(
@ -152,14 +180,6 @@ class HomeMenu(
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(
context.getString(R.string.browser_menu_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 {
val menuItems = if (FeatureFlags.toolbarMenuFeature) {
newCoreMenuItems()
} else {
oldCoreMenuItems
}
// 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.
context.components.backgroundServices.accountManagerAvailableQueue.runIfReadyOrQueue {
@ -224,9 +375,11 @@ class HomeMenu(
context.components.backgroundServices.accountManager.register(object : AccountObserver {
override fun onAuthenticationProblems() {
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged(BrowserMenuBuilder(
listOf(reconnectToSyncItem) + coreMenuItems
))
onMenuBuilderChanged(
BrowserMenuBuilder(
menuItemsWithReconnectItem
)
)
}
}
@ -234,7 +387,7 @@ class HomeMenu(
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged(
BrowserMenuBuilder(
coreMenuItems
menuItems
)
)
}
@ -244,7 +397,7 @@ class HomeMenu(
lifecycleOwner.lifecycleScope.launch(Dispatchers.Main) {
onMenuBuilderChanged(
BrowserMenuBuilder(
coreMenuItems
menuItems
)
)
}

@ -8,6 +8,7 @@ import android.content.Intent
import androidx.navigation.NavController
import mozilla.components.lib.crash.Crash
import org.mozilla.fenix.NavGraphDirections
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
/**
* 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) {
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.TabInCollectionViewHolder
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.OnboardingFinishViewHolder
import org.mozilla.fenix.home.sessioncontrol.viewholders.onboarding.OnboardingHeaderViewHolder
@ -116,6 +117,8 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
val state: OnboardingState.SignedOutCanAutoSignIn
) : AdapterItem(OnboardingAutomaticSignInViewHolder.LAYOUT_ID)
object ExperimentDefaultBrowserCard : AdapterItem(ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID)
object OnboardingThemePicker : AdapterItem(OnboardingThemePickerViewHolder.LAYOUT_ID)
object OnboardingTrackingProtection :
AdapterItem(OnboardingTrackingProtectionViewHolder.LAYOUT_ID)
@ -207,6 +210,8 @@ class SessionControlAdapter(
OnboardingToolbarPositionPickerViewHolder.LAYOUT_ID -> OnboardingToolbarPositionPickerViewHolder(
view
)
ExperimentDefaultBrowserCardViewHolder.LAYOUT_ID -> ExperimentDefaultBrowserCardViewHolder(view, interactor)
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.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.openSetDefaultBrowserOption
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -167,6 +168,16 @@ interface SessionControlController {
* @see [CollectionInteractor.onCollectionMenuOpened] and [TopSiteInteractor.onTopSiteMenuOpened]
*/
fun handleMenuOpened()
/**
* @see [ExperimentCardInteractor.onSetDefaultBrowserClicked]
*/
fun handleSetDefaultBrowser()
/**
* @see [ExperimentCardInteractor.onCloseExperimentCardClicked]
*/
fun handleCloseExperimentCard()
}
@Suppress("TooManyFunctions", "LargeClass")
@ -181,6 +192,7 @@ class DefaultSessionControlController(
private val restoreUseCase: TabsUseCases.RestoreUseCase,
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val requestDesktopSiteUseCase: SessionUseCases.RequestDesktopSiteUseCase,
private val fragmentStore: HomeFragmentStore,
private val navController: NavController,
private val viewLifecycleScope: CoroutineScope,
@ -403,6 +415,10 @@ class DefaultSessionControlController(
selectTab = true,
startLoading = true
)
if (settings.openNextTabInDesktopMode) {
activity.handleRequestDesktopMode()
}
activity.openToBrowser(BrowserDirection.FromHome)
}
@ -548,4 +564,16 @@ class DefaultSessionControlController(
)
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()
}
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.
* Provides implementations for the CollectionInteractor, OnboardingInteractor,
@ -199,7 +211,7 @@ interface TopSiteInteractor {
class SessionControlInteractor(
private val controller: SessionControlController
) : CollectionInteractor, OnboardingInteractor, TopSiteInteractor, TipInteractor,
TabSessionInteractor, ToolbarInteractor {
TabSessionInteractor, ToolbarInteractor, ExperimentCardInteractor {
override fun onCollectionAddTabTapped(collection: TabCollection) {
controller.handleCollectionAddTabTapped(collection)
}
@ -295,4 +307,12 @@ class SessionControlInteractor(
override fun onTopSiteMenuOpened() {
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 mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
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
// When we remove the tabs from the home screen this will get much simpler again.
@Suppress("ComplexMethod")
@Suppress("ComplexMethod", "LongParameterList")
private fun normalModeAdapterItems(
topSites: List<TopSite>,
collections: List<TabCollection>,
expandedCollections: Set<Long>,
tip: Tip?,
showCollectionsPlaceholder: Boolean
showCollectionsPlaceholder: Boolean,
showSetAsDefaultBrowserCard: Boolean
): List<AdapterItem> {
val items = mutableListOf<AdapterItem>()
tip?.let { items.add(AdapterItem.TipItem(it)) }
if (showSetAsDefaultBrowserCard) {
items.add(AdapterItem.ExperimentDefaultBrowserCard)
}
if (topSites.isNotEmpty()) {
items.add(AdapterItem.TopSitePager(topSites))
}
@ -71,6 +75,13 @@ private fun privateModeAdapterItems() = listOf(AdapterItem.PrivateBrowsingDescri
private fun onboardingAdapterItems(onboardingState: OnboardingState): List<AdapterItem> {
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:
items.addAll(
when (onboardingState) {
@ -90,14 +101,6 @@ private fun onboardingAdapterItems(onboardingState: OnboardingState): List<Adapt
items.addAll(
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.OnboardingFinish
)
@ -112,7 +115,8 @@ private fun HomeFragmentState.toAdapterList(): List<AdapterItem> = when (mode) {
collections,
expandedCollections,
tip,
showCollectionPlaceholder
showCollectionPlaceholder,
showSetAsDefaultBrowserCard
)
is Mode.Private -> privateModeAdapterItems()
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 org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.addUnderline
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.navigateBlockingForAsyncNavGraph
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) {
private val headerText = view.header_text
init {
val interactor = OnboardingInteractor(OnboardingController(itemView.context))
view.fxa_sign_in_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingManualSignIn)
val directions = HomeFragmentDirections.actionGlobalTurnOnSync()
Navigation.findNavController(view).navigate(directions)
}
view.learn_more.addUnderline()
view.learn_more.setOnClickListener {
interactor.onLearnMoreClicked()
Navigation.findNavController(view).navigateBlockingForAsyncNavGraph(directions)
}
}
fun bind() {
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 {

@ -18,7 +18,7 @@ class OnboardingPrivacyNoticeViewHolder(
) : RecyclerView.ViewHolder(view) {
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)
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
import android.view.View
import androidx.appcompat.widget.SwitchCompat
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.onboarding_tracking_protection.view.*
import org.mozilla.fenix.R
@ -20,29 +19,20 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
private var standardTrackingProtection: OnboardingRadioButton
private var strictTrackingProtection: OnboardingRadioButton
private var trackingProtectionToggle: SwitchCompat
init {
view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_tracking_protection)
trackingProtectionToggle = view.tracking_protection_toggle
standardTrackingProtection = view.tracking_protection_standard_option
strictTrackingProtection = view.tracking_protection_strict_default
view.description_text.text = view.context.getString(
R.string.onboarding_tracking_protection_description_2
R.string.onboarding_tracking_protection_description_3
)
trackingProtectionToggle.apply {
isChecked = view.context.settings().shouldUseTrackingProtection
setOnCheckedChangeListener { _, isChecked ->
updateTrackingProtectionSetting(isChecked)
updateRadioGroupState(isChecked)
}
}
setupRadioGroup(trackingProtectionToggle.isChecked)
updateRadioGroupState(trackingProtectionToggle.isChecked)
val isTrackingProtectionEnabled = view.context.settings().shouldUseTrackingProtection
setupRadioGroup(isTrackingProtectionEnabled)
updateRadioGroupState(isTrackingProtectionEnabled)
}
private fun setupRadioGroup(isChecked: Boolean) {
@ -74,15 +64,6 @@ class OnboardingTrackingProtectionViewHolder(view: View) : RecyclerView.ViewHold
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() {
itemView.context?.components?.let {
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.item.SimpleBrowserMenuItem
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.PINNED
import org.mozilla.fenix.R
@ -63,7 +64,7 @@ class TopSiteItemViewHolder(
fun bind(topSite: TopSite) {
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
} else {
View.GONE
@ -79,6 +80,9 @@ class TopSiteItemViewHolder(
SupportUtils.JD_URL -> {
favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_jd))
}
SupportUtils.PDD_URL -> {
favicon_image.setImageDrawable(getDrawable(itemView.context, R.drawable.ic_pdd))
}
else -> {
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.increaseTapArea
import org.mozilla.fenix.ext.loadIntoView
/**
* 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>
}
import org.mozilla.fenix.selection.SelectionHolder
import org.mozilla.fenix.selection.SelectionInteractor
class LibrarySiteItemView @JvmOverloads constructor(
context: Context,

@ -88,7 +88,7 @@ class DefaultBookmarkController(
}
override fun handleBookmarkEdit(node: BookmarkNode) {
navigate(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
navigateToGivenDirection(BookmarkFragmentDirections.actionBookmarkFragmentToBookmarkEditFragment(node.guid))
}
override fun handleBookmarkSelected(node: BookmarkNode) {
@ -118,7 +118,7 @@ class DefaultBookmarkController(
}
override fun handleBookmarkSharing(item: BookmarkNode) {
navigate(
navigateToGivenDirection(
BookmarkFragmentDirections.actionGlobalShareFragment(
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()
navController.nav(R.id.bookmarkFragment, directions)
}

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

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

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

@ -8,7 +8,7 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
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
class DownloadAdapter(

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

Loading…
Cancel
Save