Merge pull request #293 from fork-maintainers/upstream-sync

Pull in changes from Mozilla Firefox 86.1.1
fix-addon-search iceraven-1.7.0
interfect 3 years ago committed by GitHub
commit 1da7a9fac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -29,9 +29,9 @@ pull_request_rules:
merge: merge:
method: rebase method: rebase
strict: smart strict: smart
- name: Release automation - name: Release automation (Old)
conditions: conditions:
- base~=releases/.* - base~=releases[_/].*
- author=github-actions[bot] - author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet. # Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-debug - check-success=build-android-test-debug
@ -57,3 +57,32 @@ pull_request_rules:
strict: smart strict: smart
delete_head_branch: delete_head_branch:
force: false force: false
- name: Release automation (New)
conditions:
- base~=releases[_/].*
- author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-beta
- check-success=build-android-test-debug
- check-success=build-beta-firebase
- check-success=build-debug
- check-success=build-nightly-simulation
- check-success=lint-compare-locales
- check-success=lint-detekt
- check-success=lint-ktlint
- check-success=lint-lint
- check-success=signing-android-test-beta
- check-success=signing-beta-firebase
- check-success=signing-nightly-simulation
- check-success=test-debug
- check-success=ui-test-x86-beta
- files~=(AndroidComponents.kt)
actions:
review:
type: APPROVE
message: 🚢
merge:
method: rebase
strict: smart
delete_head_branch:
force: false

@ -37,8 +37,12 @@ android {
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
resValue "bool", "IS_DEBUG", "false" resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\"" // This should be the "public" base URL of AMO.
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\"" buildConfigField "String", "AMO_BASE_URL", "\"https://addons.mozilla.org\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"7dfae8669acc4312a65e8ba5553036\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"mozilla\""
// This should be the base URL used to call the AMO API.
buildConfigField "String", "AMO_SERVER_URL", "\"https://services.addons.mozilla.org\""
def deepLinkSchemeValue = "fenix-dev" def deepLinkSchemeValue = "fenix-dev"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [ manifestPlaceholders = [
@ -131,8 +135,8 @@ android {
def deepLinkSchemeValue = "iceraven-debug" def deepLinkSchemeValue = "iceraven-debug"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\"" buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// Use custom default allowed addon list // Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\"" buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\"" buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
} }
forkRelease releaseTemplate >> { forkRelease releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true" buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
@ -143,8 +147,8 @@ android {
"deepLinkScheme": deepLinkSchemeValue "deepLinkScheme": deepLinkSchemeValue
] ]
// Use custom default allowed addon list // Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\"" buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\"" buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
} }
} }
@ -466,6 +470,7 @@ dependencies {
implementation Deps.mozilla_browser_menu2 implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_search implementation Deps.mozilla_browser_search
implementation Deps.mozilla_browser_session implementation Deps.mozilla_browser_session
implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_tabstray implementation Deps.mozilla_browser_tabstray

@ -910,7 +910,7 @@ metrics:
https://github.com/mozilla-mobile/fenix/issues/1607) the value will be https://github.com/mozilla-mobile/fenix/issues/1607) the value will be
`custom`. `custom`.
`source` will be: `action`, `suggestion`, `widget` or `shortcut` `source` will be: `action`, `suggestion`, `widget`, `shortcut`, `topsite`
(depending on the source from which the search started). Also added the (depending on the source from which the search started). Also added the
`other` option for the source but it should never enter on this case. `other` option for the source but it should never enter on this case.
send_in_pings: send_in_pings:
@ -2206,6 +2206,36 @@ reader_mode:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
tabs_tray.cfr:
dismiss:
type: event
description: |
A user dismisses the tabs tray CFR.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16485
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17442
- https://github.com/mozilla-mobile/fenix/issues/16485#issuecomment-759641324
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
go_to_settings:
type: event
description: |
A user selects the CFR option to navigate to settings.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/16485
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17442
- https://github.com/mozilla-mobile/fenix/issues/16485#issuecomment-759641324
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
tabs_tray: tabs_tray:
opened: opened:
type: event type: event
@ -3385,6 +3415,19 @@ top_sites:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
open_google_search_attribution:
type: event
description: |
A user opened the google top site
bugs:
- https://github.com/mozilla-mobile/fenix/issues/17418
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/17637
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-08-01"
open_frecency: open_frecency:
type: event type: event
description: | description: |
@ -4075,6 +4118,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802 - https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity: data_sensitivity:
- technical - technical
- interaction - interaction
@ -4082,7 +4126,7 @@ storage.stats:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
app_bytes: app_bytes:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4099,6 +4143,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802 - https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity: data_sensitivity:
- technical - technical
- interaction - interaction
@ -4106,7 +4151,7 @@ storage.stats:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
cache_bytes: cache_bytes:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4120,6 +4165,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802 - https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity: data_sensitivity:
- technical - technical
- interaction - interaction
@ -4127,7 +4173,7 @@ storage.stats:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
data_dir_bytes: data_dir_bytes:
send_in_pings: send_in_pings:
- metrics - metrics
@ -4143,6 +4189,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802 - https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews: data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732 - https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity: data_sensitivity:
- technical - technical
- interaction - interaction
@ -4150,7 +4197,7 @@ storage.stats:
- fenix-core@mozilla.com - fenix-core@mozilla.com
- perf-android-fe@mozilla.com - perf-android-fe@mozilla.com
- mcomella@mozilla.com - mcomella@mozilla.com
expires: "2021-02-01" expires: "2021-08-01"
progressive_web_app: progressive_web_app:
homescreen_tap: homescreen_tap:
@ -4260,3 +4307,57 @@ tabs:
notification_emails: notification_emails:
- fenix-core@mozilla.com - fenix-core@mozilla.com
expires: "2021-08-01" expires: "2021-08-01"
contextual_menu:
copy_tapped:
type: event
description: |
The context menu's 'copy' option was used.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11580
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16968
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-06-01"
search_tapped:
type: event
description: |
The context menu's 'search' option was used.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11580
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16968
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-06-01"
select_all_tapped:
type: event
description: |
The context menu's 'select all' option was used.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11580
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16968
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-06-01"
share_tapped:
type: event
description: |
The context menu's 'share' option was used.
bugs:
- https://github.com/mozilla-mobile/fenix/issues/11580
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/16968
data_sensitivity:
- interaction
notification_emails:
- fenix-core@mozilla.com
expires: "2021-06-01"

@ -50,6 +50,7 @@
#################################################################################################### ####################################################################################################
-assumenosideeffects class android.util.Log { -assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int); public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...); public static int d(...);
} }

@ -1,5 +1,10 @@
<html> <html>
<body> <body>
<a href="../resources/Globe.svg" download>Page content: Globe.svg</a> <a id="link" href="../resources/Globe.svg" download>Page content: Globe.svg</a>
<script>
(function() {
document.getElementById("link").click()
})();
</script>
</body> </body>
</html> </html>

@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
@ -25,11 +26,13 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.ext.waitNotNull import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.mDevice import org.mozilla.fenix.ui.robots.mDevice
import java.io.File
object TestHelper { object TestHelper {
@ -120,4 +123,19 @@ object TestHelper {
0 0
) )
} }
// Remove test file from the device Downloads folder
@Suppress("Deprecation")
fun deleteDownloadFromStorage(fileName: String) {
runBlocking {
val downloadedFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
fileName
)
if (downloadedFile.exists()) {
downloadedFile.delete()
}
}
}
} }

@ -25,6 +25,7 @@ private const val EXPECTED_RUNBLOCKING_COUNT = 2
private const val EXPECTED_COMPONENT_INIT_COUNT = 42 private const val EXPECTED_COMPONENT_INIT_COUNT = 42
private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12 private const val EXPECTED_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4 private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4
private const val EXPECTED_NUMBER_OF_INFLATION = 12
private val failureMsgStrictMode = getErrorMessage( private val failureMsgStrictMode = getErrorMessage(
shortName = "StrictMode suppression", shortName = "StrictMode suppression",
@ -54,6 +55,11 @@ private val failureMsgRecyclerViewConstraintLayoutChildren = getErrorMessage(
) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " + ) + "Please note that we're not sure if this is a useful metric to assert: with your feedback, " +
"we'll find out over time if it is or is not." "we'll find out over time if it is or is not."
private val failureMsgNumberOfInflation = getErrorMessage(
shortName = "Number of inflation on start up doesn't match expected count",
implications = "The number of inflation can negatively impact start up time. Having more inflations" +
"will most likely mean we're adding extra work on the UI thread."
)
/** /**
* A performance test to limit the number of StrictMode suppressions and number of runBlocking used * A performance test to limit the number of StrictMode suppressions and number of runBlocking used
* on startup. * on startup.
@ -90,6 +96,8 @@ class StartupExcessiveResourceUseTest {
val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1) val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1)
val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null) val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
val actualNumberOfInflations = InflationCounter.inflationCount.get()
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount) assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking) assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount) assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount)
@ -99,6 +107,7 @@ class StartupExcessiveResourceUseTest {
EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN, EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
actualRecyclerViewConstraintLayoutChildren actualRecyclerViewConstraintLayoutChildren
) )
assertEquals(failureMsgNumberOfInflation, EXPECTED_NUMBER_OF_INFLATION, actualNumberOfInflations)
} }
} }

@ -395,7 +395,7 @@ class BookmarksTest {
} }
bookmarksMenu { bookmarksMenu {
verifyEmptyBookmarksList() verifyDeleteMultipleBookmarksSnackBar()
} }
} }
@ -466,20 +466,12 @@ class BookmarksTest {
bookmarksListIdlingResource = bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2) RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 2)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
}.openThreeDotMenu(defaultWebPage.url) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.clickEdit {
verifyEditBookmarksView()
changeBookmarkTitle(testBookmark.title)
saveEditBookmark()
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
createFolder(bookmarksFolderName) createFolder(bookmarksFolderName)
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!) IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.openThreeDotMenu(testBookmark.title) { }.openThreeDotMenu(defaultWebPage.title) {
}.clickEdit { }.clickEdit {
clickParentFolderSelector() clickParentFolderSelector()
selectFolder(bookmarksFolderName) selectFolder(bookmarksFolderName)

@ -52,40 +52,6 @@
// //
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587") // @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test // @Test
// fun verifyCreateFirstCollectionFlowItems() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
//
// navigationToolbar {
// }.enterURLAndEnterToBrowser(firstWebPage.url) {
// }.openHomeScreen {
// }.openNavigationToolbar {
// }.enterURLAndEnterToBrowser(secondWebPage.url) {
// }.openTabDrawer {
// }.openTabsListThreeDotMenu {
// verifySaveCollection()
// }.clickOpenTabsMenuSaveCollection {
// clickSaveCollectionButton()
// verifySelectTabsView()
// selectAllTabsForCollection()
// verifyTabsSelectedCounterText(2)
// deselectAllTabsForCollection()
// verifyTabsSelectedCounterText(0)
// selectTabForCollection(firstWebPage.title)
// verifyTabsSelectedCounterText(1)
// selectAllTabsForCollection()
// saveTabsSelectedForCollection()
// verifyNameCollectionView()
// verifyDefaultCollectionName("Collection 1")
// typeCollectionName(firstCollectionName)
// verifySnackBarText("Tabs saved!")
//// verifyExistingOpenTabs(firstWebPage.title)
//// verifyExistingOpenTabs(secondWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// // open a webpage, and add currently opened tab to existing collection // // open a webpage, and add currently opened tab to existing collection
// fun addTabToExistingCollectionTest() { // fun addTabToExistingCollectionTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) // val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -135,23 +101,6 @@
// //
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587") // @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test // @Test
// fun collectionMenuOpenAllTabsTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// createCollection(firstCollectionName)
//
// homeScreen {
// closeTab()
// expandCollection(firstCollectionName)
// clickCollectionThreeDotButton()
// selectOpenTabs()
// }.openTabDrawer {
// verifyExistingOpenTabs(firstWebPage.title)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun renameCollectionTest() { // fun renameCollectionTest() {
// createCollection(firstCollectionName) // createCollection(firstCollectionName)
// //
@ -167,20 +116,6 @@
// //
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587") // @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test // @Test
// fun deleteCollectionTest() {
// createCollection(firstCollectionName)
//
// homeScreen {
// expandCollection(firstCollectionName)
// clickCollectionThreeDotButton()
// selectDeleteCollection()
// confirmDeleteCollection()
// verifyNoCollectionsHeader()
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun createCollectionFromTabTest() { // fun createCollectionFromTabTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) // val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// //
@ -199,42 +134,6 @@
// //
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587") // @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test // @Test
// fun verifyExpandedCollectionItemsTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
// createCollection(firstCollectionName)
//
// homeScreen {
// verifyCollectionIsDisplayed(firstCollectionName)
// verifyCollectionIcon()
// expandCollection(firstCollectionName)
// verifyItemInCollectionExists(firstWebPage.title)
// verifyCollectionItemLogo()
// verifyCollectionItemUrl()
// verifyShareCollectionButtonIsVisible(true)
// verifyCollectionMenuIsVisible(true)
// verifyCollectionItemRemoveButtonIsVisible(firstWebPage.title, true)
// collapseCollection(firstCollectionName)
// verifyItemInCollectionExists(firstWebPage.title, false)
// verifyShareCollectionButtonIsVisible(false)
// verifyCollectionMenuIsVisible(false)
// verifyCollectionItemRemoveButtonIsVisible(firstWebPage.title, false)
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun shareCollectionTest() {
// createCollection(firstCollectionName)
// homeScreen {
// expandCollection(firstCollectionName)
// clickShareCollectionButton()
// verifyShareTabsOverlay()
// }
// }
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @Test
// fun removeTabFromCollectionTest() { // fun removeTabFromCollectionTest() {
// val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) // val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
// //

@ -4,25 +4,21 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import android.os.Environment
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.ui.robots.downloadRobot import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.notificationShade import org.mozilla.fenix.ui.robots.notificationShade
import java.io.File
/** /**
* Tests for verifying basic functionality of download prompt UI * Tests for verifying basic functionality of download prompt UI
@ -56,35 +52,20 @@ class DownloadTest {
} }
} }
@Suppress("Deprecation")
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
// Clear Download TestHelper.deleteDownloadFromStorage("Globe.svg")
runBlocking {
val downloadedFile = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),
"Globe.svg.html"
)
if (downloadedFile.exists()) {
downloadedFile.delete()
}
}
} }
@Test @Test
@Ignore("Temp disable flaky test - see: https://github.com/mozilla-mobile/fenix/issues/10798")
fun testDownloadPrompt() { fun testDownloadPrompt() {
homeScreen { }.dismissOnboarding()
val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer) val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer)
navigationToolbar { navigationToolbar {
}.openNewTabAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle() mDevice.waitForIdle()
clickLinkMatchingText(defaultWebPage.content)
} }
downloadRobot { downloadRobot {
@ -97,9 +78,8 @@ class DownloadTest {
val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer) val defaultWebPage = TestAssetHelper.getDownloadAsset(mockWebServer)
navigationToolbar { navigationToolbar {
}.openNewTabAndEnterToBrowser(defaultWebPage.url) { }.enterURLAndEnterToBrowser(defaultWebPage.url) {
mDevice.waitForIdle() mDevice.waitForIdle()
clickLinkMatchingText(defaultWebPage.content)
} }
downloadRobot { downloadRobot {

@ -58,9 +58,9 @@ class NavigationToolbarTest {
mDevice.waitForIdle() mDevice.waitForIdle()
}.openNavigationToolbar { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(nextWebPage.url) { }.enterURLAndEnterToBrowser(nextWebPage.url) {
mDevice.waitForIdle()
verifyUrl(nextWebPage.url.toString()) verifyUrl(nextWebPage.url.toString())
mDevice.pressBack() }.openThreeDotMenu {
}.goBack {
mDevice.waitForIdle() mDevice.waitForIdle()
verifyUrl(defaultWebPage.url.toString()) verifyUrl(defaultWebPage.url.toString())
} }

@ -8,11 +8,9 @@ import android.view.View
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.readerViewRobot
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
@ -30,10 +28,10 @@ import org.mozilla.fenix.ui.robots.mDevice
* *
*/ */
@Ignore("Temp disable - reader view page detection issues: https://github.com/mozilla-mobile/fenix/issues/9688 ") // @Ignore("Temp disable - reader view page detection issues: https://github.com/mozilla-mobile/fenix/issues/9688 ")
class ReaderViewTest { class ReaderViewTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private var readerViewNotificationDot: ViewVisibilityIdlingResource? = null private var readerViewNotification: ViewVisibilityIdlingResource? = null
@get:Rule @get:Rule
val activityIntentTestRule = HomeActivityIntentTestRule() val activityIntentTestRule = HomeActivityIntentTestRule()
@ -44,24 +42,18 @@ class ReaderViewTest {
dispatcher = AndroidAssetDispatcher() dispatcher = AndroidAssetDispatcher()
start() start()
} }
readerViewNotificationDot = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.notification_dot),
View.VISIBLE
)
} }
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
IdlingRegistry.getInstance().unregister(readerViewNotificationDot) IdlingRegistry.getInstance().unregister(readerViewNotification)
} }
/** /**
* Verify that Reader View capable pages * Verify that Reader View capable pages
* *
* - Show blue notification in the three dot menu * - Show the toggle button in the navigation bar
* - Show the toggle button in the three dot menu
* *
*/ */
@Test @Test
@ -74,23 +66,22 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
IdlingRegistry.getInstance().register(readerViewNotificationDot) readerViewNotification = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
readerViewRobot { IdlingRegistry.getInstance().register(readerViewNotification)
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { verifyReaderViewDetected(true)
verifyReaderViewToggle(true) }
}.closeBrowserMenuToBrowser { }
} }
/** /**
* Verify that non Reader View capable pages * Verify that non Reader View capable pages
* *
* - Do not show a blue notification in the three dot menu * - Reader View toggle should not be visible in the navigation toolbar
* - Reader View toggle should not be visible in the three dot menu
* *
*/ */
@Test @Test
@ -103,15 +94,9 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
readerViewRobot { navigationToolbar {
verifyReaderViewDetected(false) verifyReaderViewDetected(false)
} }
navigationToolbar {
}.openThreeDotMenu {
verifyReaderViewToggle(false)
verifyReaderViewAppearance(false)
}.closeBrowserMenuToBrowser { }
} }
@Test @Test
@ -124,61 +109,25 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
IdlingRegistry.getInstance().register(readerViewNotificationDot) readerViewNotification = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
readerViewRobot { IdlingRegistry.getInstance().register(readerViewNotification)
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { verifyReaderViewDetected(true)
verifyReaderViewToggle(true) toggleReaderView()
}.toggleReaderView {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.toggleReaderView { }.closeBrowserMenuToBrowser { }
}.openThreeDotMenu {
verifyReaderViewAppearance(false)
}.close { }
readerViewRobot {
verifyReaderViewDetected(false)
}
}
@Test
fun verifyReaderViewAppearanceUI() {
val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(readerViewPage.url) {
mDevice.waitForIdle()
}
IdlingRegistry.getInstance().register(readerViewNotificationDot)
readerViewRobot {
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
toggleReaderView()
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewToggle(true) verifyReaderViewAppearance(false)
}.toggleReaderView { }.close { }
}.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.openReaderViewAppearance {
verifyAppearanceFontGroup(true)
verifyAppearanceFontSansSerif(true)
verifyAppearanceFontSerif(true)
verifyAppearanceFontIncrease(true)
verifyAppearanceFontDecrease(true)
verifyAppearanceColorGroup(true)
verifyAppearanceColorDark(true)
verifyAppearanceColorLight(true)
verifyAppearanceColorSepia(true)
}
} }
@Test @Test
@ -191,16 +140,16 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
IdlingRegistry.getInstance().register(readerViewNotificationDot) readerViewNotification = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
readerViewRobot { IdlingRegistry.getInstance().register(readerViewNotification)
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { verifyReaderViewDetected(true)
verifyReaderViewToggle(true) toggleReaderView()
}.toggleReaderView {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {
@ -226,16 +175,16 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
IdlingRegistry.getInstance().register(readerViewNotificationDot) readerViewNotification = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
readerViewRobot { IdlingRegistry.getInstance().register(readerViewNotification)
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { verifyReaderViewDetected(true)
verifyReaderViewToggle(true) toggleReaderView()
}.toggleReaderView {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {
@ -267,16 +216,16 @@ class ReaderViewTest {
mDevice.waitForIdle() mDevice.waitForIdle()
} }
IdlingRegistry.getInstance().register(readerViewNotificationDot) readerViewNotification = ViewVisibilityIdlingResource(
activityIntentTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
readerViewRobot { IdlingRegistry.getInstance().register(readerViewNotification)
verifyReaderViewDetected(true)
}
navigationToolbar { navigationToolbar {
}.openThreeDotMenu { verifyReaderViewDetected(true)
verifyReaderViewToggle(true) toggleReaderView()
}.toggleReaderView {
}.openThreeDotMenu { }.openThreeDotMenu {
verifyReaderViewAppearance(true) verifyReaderViewAppearance(true)
}.openReaderViewAppearance { }.openReaderViewAppearance {

@ -120,6 +120,7 @@ class SettingsBasicsTest {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(page2.url) { }.enterURLAndEnterToBrowser(page2.url) {
verifyUrl(page2.url.toString())
}.openThreeDotMenu { }.openThreeDotMenu {
clickAddBookmarkButton() clickAddBookmarkButton()
} }

@ -5,25 +5,34 @@
package org.mozilla.fenix.ui package org.mozilla.fenix.ui
import android.view.View import android.view.View
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer import okhttp3.mockwebserver.MockWebServer
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Ignore
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.clickUrlbar import org.mozilla.fenix.ui.robots.clickUrlbar
import org.mozilla.fenix.ui.robots.downloadRobot
import org.mozilla.fenix.ui.robots.enhancedTrackingProtection
import org.mozilla.fenix.ui.robots.homeScreen import org.mozilla.fenix.ui.robots.homeScreen
import org.mozilla.fenix.ui.robots.navigationToolbar import org.mozilla.fenix.ui.robots.navigationToolbar
import org.mozilla.fenix.ui.robots.tabDrawer
/** /**
* Test Suite that contains tests defined as part of the Smoke and Sanity check defined in Test rail. * Test Suite that contains tests defined as part of the Smoke and Sanity check defined in Test rail.
@ -35,6 +44,16 @@ class SmokeTest {
private lateinit var mockWebServer: MockWebServer private lateinit var mockWebServer: MockWebServer
private var awesomeBar: ViewVisibilityIdlingResource? = null private var awesomeBar: ViewVisibilityIdlingResource? = null
private var searchSuggestionsIdlingResource: RecyclerViewIdlingResource? = null private var searchSuggestionsIdlingResource: RecyclerViewIdlingResource? = null
private var addonsListIdlingResource: RecyclerViewIdlingResource? = null
private var recentlyClosedTabsListIdlingResource: RecyclerViewIdlingResource? = null
private var readerViewNotification: ViewVisibilityIdlingResource? = null
private val downloadFileName = "Globe.svg"
private val searchEngine = object {
var title = "Ecosia"
var url = "https://www.ecosia.org/search?q=%s"
}
val collectionName = "First Collection"
private var bookmarksListIdlingResource: RecyclerViewIdlingResource? = null
// This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null // This finds the dialog fragment child of the homeFragment, otherwise the awesomeBar would return null
private fun getAwesomebarView(): View? { private fun getAwesomebarView(): View? {
@ -48,6 +67,12 @@ class SmokeTest {
@get:Rule @get:Rule
val activityTestRule = HomeActivityTestRule() val activityTestRule = HomeActivityTestRule()
@get:Rule
var mGrantPermissions = GrantPermissionRule.grant(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
)
@Before @Before
fun setUp() { fun setUp() {
mockWebServer = MockWebServer().apply { mockWebServer = MockWebServer().apply {
@ -59,6 +84,32 @@ class SmokeTest {
@After @After
fun tearDown() { fun tearDown() {
mockWebServer.shutdown() mockWebServer.shutdown()
if (awesomeBar != null) {
IdlingRegistry.getInstance().unregister(awesomeBar!!)
}
if (searchSuggestionsIdlingResource != null) {
IdlingRegistry.getInstance().unregister(searchSuggestionsIdlingResource!!)
}
if (addonsListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!)
}
if (recentlyClosedTabsListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
}
deleteDownloadFromStorage(downloadFileName)
if (bookmarksListIdlingResource != null) {
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}
if (readerViewNotification != null) {
IdlingRegistry.getInstance().unregister(readerViewNotification)
}
} }
// copied over from HomeScreenTest // copied over from HomeScreenTest
@ -112,6 +163,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the functionality of the onboarding Start Browsing button
fun startBrowsingButtonTest() { fun startBrowsingButtonTest() {
homeScreen { homeScreen {
verifyStartBrowsingButton() verifyStartBrowsingButton()
@ -121,6 +173,13 @@ class SmokeTest {
} }
@Test @Test
/* Verifies the nav bar:
- opening a web page
- the existence of nav bar items
- editing the url bar
- the tab drawer button
- opening a new search and dismissing the nav bar
*/
fun verifyBasicNavigationToolbarFunctionality() { fun verifyBasicNavigationToolbarFunctionality() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -141,6 +200,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the list of items in a tab's 3 dot menu
fun verifyPageMainMenuItemsTest() { fun verifyPageMainMenuItemsTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -153,6 +213,7 @@ class SmokeTest {
// Could be removed when more smoke tests from the History category are added // Could be removed when more smoke tests from the History category are added
@Test @Test
// Verifies the History menu opens from a tab's 3 dot menu
fun openMainMenuHistoryItemTest() { fun openMainMenuHistoryItemTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -163,6 +224,7 @@ class SmokeTest {
// Could be removed when more smoke tests from the Bookmarks category are added // Could be removed when more smoke tests from the Bookmarks category are added
@Test @Test
// Verifies the Bookmarks menu opens from a tab's 3 dot menu
fun openMainMenuBookmarksItemTest() { fun openMainMenuBookmarksItemTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -172,6 +234,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Synced tabs menu opens from a tab's 3 dot menu
fun openMainMenuSyncedTabsItemTest() { fun openMainMenuSyncedTabsItemTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -182,6 +245,7 @@ class SmokeTest {
// Could be removed when more smoke tests from the Settings category are added // Could be removed when more smoke tests from the Settings category are added
@Test @Test
// Verifies the Settings menu opens from a tab's 3 dot menu
fun openMainMenuSettingsItemTest() { fun openMainMenuSettingsItemTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -191,6 +255,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Find in page option in a tab's 3 dot menu
fun openMainMenuFindInPageTest() { fun openMainMenuFindInPageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -203,6 +268,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Add to top sites option in a tab's 3 dot menu
fun openMainMenuAddTopSiteTest() { fun openMainMenuAddTopSiteTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -219,22 +285,31 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Add to home screen option in a tab's 3 dot menu
fun mainMenuAddToHomeScreenTest() { fun mainMenuAddToHomeScreenTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar { homeScreen {
}.enterURLAndEnterToBrowser(defaultWebPage.url) { }.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
}.openAddToHomeScreen {
clickCancelShortcutButton()
}
browserScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
}.openAddToHomeScreen { }.openAddToHomeScreen {
verifyShortcutNameField(defaultWebPage.title) verifyShortcutNameField("Test_Page_1")
addShortcutName("Test Page")
clickAddShortcutButton() clickAddShortcutButton()
clickAddAutomaticallyButton() clickAddAutomaticallyButton()
}.openHomeScreenShortcut(defaultWebPage.title) { }.openHomeScreenShortcut("Test Page") {
verifyPageContent(defaultWebPage.content)
} }
} }
@Test @Test
// Verifies the Add to collection option in a tab's 3 dot menu
fun openMainMenuAddToCollectionTest() { fun openMainMenuAddToCollectionTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -247,6 +322,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Bookmark button in a tab's 3 dot menu
fun mainMenuBookmarkButtonTest() { fun mainMenuBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -259,6 +335,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the Share button in a tab's 3 dot menu
fun mainMenuShareButtonTest() { fun mainMenuShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -271,6 +348,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies the refresh button in a tab's 3 dot menu
fun mainMenuRefreshButtonTest() { fun mainMenuRefreshButtonTest() {
val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer) val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
@ -286,6 +364,7 @@ class SmokeTest {
} }
@Test @Test
// Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar
fun verifyETPShieldNotDisplayedIfOFFGlobally() { fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -313,6 +392,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies changing the default engine from the Search Shortcut menu
fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() { fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -361,6 +441,7 @@ class SmokeTest {
} }
@Test @Test
// Ads a new search engine from the list of custom engines
fun addPredefinedSearchEngineTest() { fun addPredefinedSearchEngineTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -380,8 +461,9 @@ class SmokeTest {
} }
@Test @Test
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
fun toggleSearchSuggestions() { fun toggleSearchSuggestions() {
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
homeScreen { homeScreen {
}.openNavigationToolbar { }.openNavigationToolbar {
typeSearchTerm("mozilla") typeSearchTerm("mozilla")
@ -412,7 +494,34 @@ class SmokeTest {
} }
} }
@Ignore("Failing, see: https://github.com/mozilla-mobile/fenix/issues/17847")
@Test
// Verifies setting as default a customized search engine name and URL
fun editCustomSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
}.openSettings {
}.openSearchSubMenu {
openAddSearchEngineMenu()
selectAddCustomSearchEngine()
typeCustomEngineDetails(searchEngine.title, searchEngine.url)
saveNewSearchEngine()
openEngineOverflowMenu("Ecosia")
clickEdit()
typeCustomEngineDetails("Test", searchEngine.url)
saveEditSearchEngine()
changeDefaultSearchEngine("Test")
}.goBack {
}.goBack {
}.openSearch {
verifyDefaultSearchEngine("Test")
clickSearchEngineShortcutButton()
verifyEnginesListShortcutContains("Test")
}
}
@Test @Test
// Swipes the nav bar left/right to switch between tabs
fun swipeToSwitchTabTest() { fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1) val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2) val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -430,6 +539,7 @@ class SmokeTest {
} }
@Test @Test
// Saves a login, then changes it and verifies the update
fun updateSavedLoginTest() { fun updateSavedLoginTest() {
val saveLoginTest = val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer) TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -461,6 +571,7 @@ class SmokeTest {
} }
@Test @Test
// Verifies that you can go to System settings and change app's permissions from inside the app
fun redirectToAppPermissionsSystemSettingsTest() { fun redirectToAppPermissionsSystemSettingsTest() {
homeScreen { homeScreen {
}.openThreeDotMenu { }.openThreeDotMenu {
@ -490,4 +601,563 @@ class SmokeTest {
verifyUnblockedByAndroid() verifyUnblockedByAndroid()
} }
} }
@Test
// 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
activityTestRule.activity.settings().setStrictETP()
val addonName = "uBlock Origin"
val trackingProtectionPage =
TestAssetHelper.getEnhancedTrackingProtectionAsset(mockWebServer)
homeScreen {
}.openThreeDotMenu {
}.openAddonsManagerMenu {
addonsListIdlingResource =
RecyclerViewIdlingResource(
activityTestRule.activity.findViewById(R.id.add_ons_list),
1
)
IdlingRegistry.getInstance().register(addonsListIdlingResource!!)
clickInstallAddon(addonName)
acceptInstallAddon()
verifyDownloadAddonPrompt(addonName, activityTestRule)
IdlingRegistry.getInstance().unregister(addonsListIdlingResource!!)
}.goBack {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(trackingProtectionPage.url) {}
enhancedTrackingProtection {
verifyEnhancedTrackingProtectionNotice()
}.closeNotificationPopup {}
browserScreen {
}.openThreeDotMenu {
}.openReportSiteIssue {
verifyUrl("webcompat.com/issues/new")
}
}
@Test
// This test verifies the Recently Closed Tabs List and items
fun verifyRecentlyClosedTabsListTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsPageTitle("Test_Page_1")
verifyRecentlyClosedTabsUrl(website.url)
}
}
@Test
// Verifies the items from the overflow menu of Recently Closed Tabs
fun recentlyClosedTabsMenuItemsTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuCopy()
verifyRecentlyClosedTabsMenuShare()
verifyRecentlyClosedTabsMenuNewTab()
verifyRecentlyClosedTabsMenuPrivateTab()
verifyRecentlyClosedTabsMenuDelete()
}
}
@Test
// Verifies the Copy option from the Recently Closed Tabs overflow menu
fun copyRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuCopy()
clickCopyRecentlyClosedTabs()
verifyCopyRecentlyClosedTabsSnackBarText()
}
}
@Test
// Verifies the Share option from the Recently Closed Tabs overflow menu
fun shareRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuShare()
clickShareRecentlyClosedTabs()
verifyShareOverlay()
verifyShareTabTitle("Test_Page_1")
verifyShareTabUrl(website.url)
verifyShareTabFavicon()
}
}
@Test
// Verifies the Open in a new tab option from the Recently Closed Tabs overflow menu
fun openRecentlyClosedTabsInNewTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuNewTab()
}.clickOpenInNewTab {
verifyUrl(website.url.toString())
}.openTabDrawer {
verifyNormalModeSelected()
}
}
@Test
// Verifies the Open in a private tab option from the Recently Closed Tabs overflow menu
fun openRecentlyClosedTabsInNewPrivateTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuPrivateTab()
}.clickOpenInPrivateTab {
verifyUrl(website.url.toString())
}.openTabDrawer {
verifyPrivateModeSelected()
}
}
@Test
// Verifies the delete option from the Recently Closed Tabs overflow menu
fun deleteRecentlyClosedTabsItemTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
closeTab()
}.openTabDrawer {
}.openRecentlyClosedTabs {
waitForListToExist()
recentlyClosedTabsListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.recently_closed_list), 1)
IdlingRegistry.getInstance().register(recentlyClosedTabsListIdlingResource!!)
verifyRecentlyClosedTabsMenuView()
IdlingRegistry.getInstance().unregister(recentlyClosedTabsListIdlingResource!!)
openRecentlyClosedTabsThreeDotMenu()
verifyRecentlyClosedTabsMenuDelete()
clickDeleteCopyRecentlyClosedTabs()
verifyEmptyRecentlyClosedTabsList()
}
}
@Test
/* Verifies downloads in the Downloads Menu:
- downloads appear in the list
- deleting a download from device storage, removes it from the Downloads Menu too
*/
fun manageDownloadsInDownloadsMenuTest() {
val downloadWebPage = TestAssetHelper.getDownloadAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(downloadWebPage.url) {
mDevice.waitForIdle()
}
downloadRobot {
verifyDownloadPrompt()
}.clickDownload {
mDevice.waitForIdle()
verifyDownloadNotificationPopup()
}
browserScreen {
}.openThreeDotMenu {
}.openDownloadsManager {
waitForDownloadsListToExist()
verifyDownloadedFileName(downloadFileName)
verifyDownloadedFileIcon()
deleteDownloadFromStorage(downloadFileName)
}.exitDownloadsManagerToBrowser {
}.openThreeDotMenu {
}.openDownloadsManager {
verifyEmptyDownloadsList()
}
}
@Test
fun createFirstCollectionTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
navigationToolbar {
}.enterURLAndEnterToBrowser(firstWebPage.url) {
mDevice.waitForIdle()
}.openTabDrawer {
}.openNewTab {
}.submitQuery(secondWebPage.url.toString()) {
mDevice.waitForIdle()
}.goToHomescreen {
}.clickSaveTabsToCollectionButton {
selectTab(firstWebPage.title)
selectTab(secondWebPage.title)
clickSaveCollection()
typeCollectionName(collectionName)
verifySnackBarText("Collection saved!")
snackBarButtonClick("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
verifyCollectionIcon()
}
}
@Test
fun verifyExpandedCollectionItemsTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
verifyCollectionIsDisplayed(collectionName)
verifyCollectionIcon()
expandCollection(collectionName)
verifyTabSavedInCollection(webPage.title)
verifyCollectionTabLogo()
verifyCollectionTabUrl()
verifyShareCollectionButtonIsVisible(true)
verifyCollectionMenuIsVisible(true)
verifyCollectionItemRemoveButtonIsVisible(webPage.title, true)
collapseCollection(collectionName)
verifyTabSavedInCollection(webPage.title, false)
verifyShareCollectionButtonIsVisible(false)
verifyCollectionMenuIsVisible(false)
}
}
@Test
fun openAllTabsInCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName)
closeTab()
}
browserScreen {
}.goToHomescreen {
expandCollection(collectionName)
clickCollectionThreeDotButton()
selectOpenTabs()
}
tabDrawer {
verifyExistingOpenTabs(webPage.title)
}
}
@Test
fun shareCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
expandCollection(collectionName)
clickShareCollectionButton()
verifyShareTabsOverlay()
}
}
@Test
fun deleteCollectionTest() {
val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(webPage.url) {
}.openTabDrawer {
createCollection(webPage.title, collectionName)
snackBarButtonClick("VIEW")
}
homeScreen {
expandCollection(collectionName)
clickCollectionThreeDotButton()
selectDeleteCollection()
confirmDeleteCollection()
verifyNoCollectionsText()
}
}
@Test
// Verifies that deleting a Bookmarks folder also removes the item from inside it.
fun deleteNonEmptyBookmarkFolderTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
browserScreen {
createBookmark(website.url)
}.openThreeDotMenu {
}.openBookmarks {
bookmarksListIdlingResource =
RecyclerViewIdlingResource(activityTestRule.activity.findViewById(R.id.bookmark_list), 1)
IdlingRegistry.getInstance().register(bookmarksListIdlingResource!!)
verifyBookmarkTitle("Test_Page_1")
createFolder("My Folder")
verifyFolderTitle("My Folder")
IdlingRegistry.getInstance().unregister(bookmarksListIdlingResource!!)
}.openThreeDotMenu("Test_Page_1") {
}.clickEdit {
clickParentFolderSelector()
selectFolder("My Folder")
navigateUp()
saveEditBookmark()
}.openThreeDotMenu("My Folder") {
}.clickDelete {
cancelFolderDeletion()
verifyFolderTitle("My Folder")
}.openThreeDotMenu("My Folder") {
}.clickDelete {
confirmFolderDeletion()
verifyDeleteSnackBarText()
navigateUp()
}
browserScreen {
}.openThreeDotMenu {
verifyBookmarksButton()
}
}
@Test
fun shareTabsFromTabsTrayTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
verifyNormalModeSelected()
verifyExistingTabList()
verifyExistingOpenTabs("Test_Page_1")
verifyTabTrayOverflowMenu(true)
}.openTabsListThreeDotMenu {
verifyShareAllTabsButton()
clickShareAllTabsButton()
verifyShareTabsOverlay()
}
}
@Test
fun emptyTabsTrayViewPrivateBrowsingTest() {
homeScreen {
}.dismissOnboarding()
homeScreen {
}.openTabDrawer {
}.toggleToPrivateTabs() {
verifyPrivateModeSelected()
verifyNormalBrowsingButtonIsDisplayed()
verifyNoTabsOpened()
verifyTabTrayOverflowMenu(true)
verifyNewTabButton()
}.openTabsListThreeDotMenu {
verifyTabSettingsButton()
verifyRecentlyClosedTabsButton()
}
}
@Test
fun privateTabsTrayWithOpenedTabTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openTabDrawer {
verifyPrivateModeSelected()
verifyNormalBrowsingButtonIsDisplayed()
verifyExistingTabList()
verifyExistingOpenTabs("Test_Page_1")
verifyCloseTabsButton("Test_Page_1")
verifyOpenedTabThumbnail()
verifyBrowserTabsTrayURL("localhost")
verifyTabTrayOverflowMenu(true)
verifyNewTabButton()
}
}
@Test
fun noHistoryInPrivateBrowsingTest() {
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
homeScreen {
}.togglePrivateBrowsingMode()
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
mDevice.waitForIdle()
}.openThreeDotMenu {
}.openHistory {
verifyEmptyHistoryView()
}
}
@Test
fun addPrivateBrowsingShortcutTest() {
homeScreen {
}.dismissOnboarding()
homeScreen {
}.triggerPrivateBrowsingShortcutPrompt {
verifyNoThanksPrivateBrowsingShortcutButton()
verifyAddPrivateBrowsingShortcutButton()
clickAddPrivateBrowsingShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("Private Firefox Preview") {
}
}
@Test
fun mainMenuInstallPWATest() {
val pwaPage = "https://rpappalax.github.io/testapp/"
navigationToolbar {
}.enterURLAndEnterToBrowser(pwaPage.toUri()) {
verifyNotificationDotOnMainMenu()
}.openThreeDotMenu {
}.clickInstall {
clickAddAutomaticallyButton()
}.openHomeScreenShortcut("yay app") {
mDevice.waitForIdle()
verifyNavURLBarHidden()
}
}
@Test
// Verifies that reader mode is detected and the custom appearance controls are displayed
fun verifyReaderViewAppearanceUI() {
val readerViewPage =
TestAssetHelper.getLoremIpsumAsset(mockWebServer)
navigationToolbar {
}.enterURLAndEnterToBrowser(readerViewPage.url) {
org.mozilla.fenix.ui.robots.mDevice.waitForIdle()
}
readerViewNotification = ViewVisibilityIdlingResource(
activityTestRule.activity.findViewById(R.id.mozac_browser_toolbar_page_actions),
View.VISIBLE
)
IdlingRegistry.getInstance().register(readerViewNotification)
navigationToolbar {
verifyReaderViewDetected(true)
toggleReaderView()
}.openThreeDotMenu {
verifyReaderViewAppearance(true)
}.openReaderViewAppearance {
verifyAppearanceFontGroup(true)
verifyAppearanceFontSansSerif(true)
verifyAppearanceFontSerif(true)
verifyAppearanceFontIncrease(true)
verifyAppearanceFontDecrease(true)
verifyAppearanceColorGroup(true)
verifyAppearanceColorDark(true)
verifyAppearanceColorLight(true)
verifyAppearanceColorSepia(true)
}
}
} }

@ -9,18 +9,16 @@ import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.clearText import androidx.test.espresso.action.ViewActions.clearText
import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.By.textContains import androidx.test.uiautomator.By.textContains
import androidx.test.uiautomator.UiScrollable import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -31,7 +29,11 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/ */
class AddToHomeScreenRobot { class AddToHomeScreenRobot {
fun verifyShortcutIcon() = assertShortcutIcon() fun verifyAddPrivateBrowsingShortcutButton() = assertAddPrivateBrowsingShortcutButton()
fun verifyNoThanksPrivateBrowsingShortcutButton() = assertNoThanksPrivateBrowsingShortcutButton()
fun clickAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton().click()
fun addShortcutName(title: String) { fun addShortcutName(title: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add to Home screen")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Add to Home screen")), waitingTime)
@ -44,6 +46,8 @@ class AddToHomeScreenRobot {
fun clickAddShortcutButton() = addButton().click() fun clickAddShortcutButton() = addButton().click()
fun clickCancelShortcutButton() = cancelAddToHomeScreenButton().click()
fun clickAddAutomaticallyButton() { fun clickAddAutomaticallyButton() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDevice.wait(Until.findObject(textContains("add automatically")), waitingTime) mDevice.wait(Until.findObject(textContains("add automatically")), waitingTime)
@ -92,15 +96,19 @@ private fun assertShortcutNameField(expectedText: String) {
.check(matches(isCompletelyDisplayed())) .check(matches(isCompletelyDisplayed()))
} }
private fun addButton() = onView(anyOf(withText("ADD"))) private fun addButton() = onView((withText("ADD")))
private fun cancelAddToHomeScreenButton() = onView((withText("CANCEL")))
private fun addAutomaticallyButton() = private fun addAutomaticallyButton() =
mDevice.findObject(UiSelector().textContains("add automatically")) mDevice.findObject(UiSelector().textContains("add automatically"))
private fun assertShortcutIcon() { private fun addPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_pos_button))
mDevice.wait(
Until.findObject(text("Firefox Preview")), private fun assertAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton()
waitingTime .check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
)
Assert.assertTrue(mDevice.hasObject(By.text("Firefox Preview"))) private fun noThanksPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_neg_button))
}
private fun assertNoThanksPrivateBrowsingShortcutButton() = noThanksPrivateBrowsingShortcutButton()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))

@ -23,12 +23,12 @@ import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.Until
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.res import androidx.test.uiautomator.By.res
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
@ -52,7 +52,7 @@ class BookmarksRobot {
assertBookmarksView() assertBookmarksView()
} }
fun verifyEmptyBookmarksList() = assertEmptyBookmarksList() fun verifyDeleteMultipleBookmarksSnackBar() = assertSnackBarText("Bookmarks deleted")
fun verifyBookmarkFavicon(forUrl: Uri) = assertBookmarkFavicon(forUrl) fun verifyBookmarkFavicon(forUrl: Uri) = assertBookmarkFavicon(forUrl)
@ -119,6 +119,13 @@ class BookmarksRobot {
fun verifyDeleteFolderConfirmationMessage() = assertDeleteFolderConfirmationMessage() fun verifyDeleteFolderConfirmationMessage() = assertDeleteFolderConfirmationMessage()
fun cancelFolderDeletion() {
onView(withText("CANCEL"))
.inRoot(RootMatchers.isDialog())
.check(matches(isDisplayed()))
.click()
}
fun createFolder(name: String) { fun createFolder(name: String) {
clickAddFolderButton() clickAddFolderButton()
addNewFolderName(name) addNewFolderName(name)
@ -133,8 +140,6 @@ class BookmarksRobot {
addFolderButton().click() addFolderButton().click()
} }
fun clickdeleteBookmarkButton() = deleteBookmarkButton().click()
fun addNewFolderName(name: String) { fun addNewFolderName(name: String) {
addFolderTitleField() addFolderTitleField()
.click() .click()
@ -167,7 +172,7 @@ class BookmarksRobot {
fun saveEditBookmark() { fun saveEditBookmark() {
saveBookmarkButton().click() saveBookmarkButton().click()
mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime) mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/bookmark_list")).waitForExists(waitingTime)
} }
fun clickParentFolderSelector() = bookmarkFolderSelector().click() fun clickParentFolderSelector() = bookmarkFolderSelector().click()
@ -191,31 +196,6 @@ class BookmarksRobot {
return Transition() return Transition()
} }
fun goBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
closeButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun confirmBookmarkFolderDeletionAndGoBackToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withText(R.string.delete_browsing_data_prompt_allow))
.inRoot(RootMatchers.isDialog())
.check(matches(isDisplayed()))
.click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openThreeDotMenu(interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
mDevice.waitNotNull(Until.findObject(res("org.mozilla.fenix.debug:id/overflow_menu")))
threeDotMenu().click()
ThreeDotMenuBookmarksRobot().interact()
return ThreeDotMenuBookmarksRobot.Transition()
}
fun openThreeDotMenu(bookmarkTitle: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition { fun openThreeDotMenu(bookmarkTitle: String, interact: ThreeDotMenuBookmarksRobot.() -> Unit): ThreeDotMenuBookmarksRobot.Transition {
mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu"))) mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu")))
threeDotMenu(bookmarkTitle).click() threeDotMenu(bookmarkTitle).click()
@ -260,7 +240,7 @@ private fun bookmarkFavicon(url: String) = onView(
) )
) )
private fun bookmarkURL(url: String) = onView(allOf(withId(R.id.url), withText(url))) private fun bookmarkURL(url: String) = onView(allOf(withId(R.id.url), withText(containsString(url))))
private fun addFolderButton() = onView(withId(R.id.add_bookmark_folder)) private fun addFolderButton() = onView(withId(R.id.add_bookmark_folder))
@ -268,8 +248,6 @@ private fun addFolderTitleField() = onView(withId(R.id.bookmarkNameEdit))
private fun saveFolderButton() = onView(withId(R.id.confirm_add_folder_button)) private fun saveFolderButton() = onView(withId(R.id.confirm_add_folder_button))
private fun deleteBookmarkButton() = onView(withId(R.id.delete_bookmark_button))
private fun threeDotMenu(bookmarkUrl: Uri) = onView( private fun threeDotMenu(bookmarkUrl: Uri) = onView(
allOf( allOf(
withId(R.id.overflow_menu), withId(R.id.overflow_menu),
@ -284,8 +262,6 @@ private fun threeDotMenu(bookmarkTitle: String) = onView(
) )
) )
private fun threeDotMenu() = onView(withId(R.id.overflow_menu)).check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
private fun snackBarText() = onView(withId(R.id.snackbar_text)) private fun snackBarText() = onView(withId(R.id.snackbar_text))
private fun snackBarUndoButton() = onView(withId(R.id.snackbar_btn)) private fun snackBarUndoButton() = onView(withId(R.id.snackbar_btn))
@ -307,7 +283,7 @@ private fun assertBookmarksView() {
withParent(withId(R.id.navigationToolbar)) withParent(withId(R.id.navigationToolbar))
) )
) )
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(isDisplayed()))
} }
private fun assertEmptyBookmarksList() = private fun assertEmptyBookmarksList() =
@ -322,7 +298,7 @@ private fun assertBookmarkFavicon(forUrl: Uri) = bookmarkFavicon(forUrl.toString
) )
private fun assertBookmarkURL(expectedURL: String) = private fun assertBookmarkURL(expectedURL: String) =
mDevice.findObject(UiSelector().text(expectedURL)) bookmarkURL(expectedURL).check(matches(isDisplayed()))
private fun assertFolderTitle(expectedTitle: String) = private fun assertFolderTitle(expectedTitle: String) =
onView(withText(expectedTitle)).check(matches(isDisplayed())) onView(withText(expectedTitle)).check(matches(isDisplayed()))
@ -360,13 +336,13 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
) )
private fun assertShareOverlay() = private fun assertShareOverlay() =
onView(withId(R.id.shareWrapper)).check(matches(ViewMatchers.isDisplayed())) onView(withId(R.id.shareWrapper)).check(matches(isDisplayed()))
private fun assertShareBookmarkTitle() = private fun assertShareBookmarkTitle() =
onView(withId(R.id.share_tab_title)).check(matches(ViewMatchers.isDisplayed())) onView(withId(R.id.share_tab_title)).check(matches(isDisplayed()))
private fun assertShareBookmarkFavicon() = private fun assertShareBookmarkFavicon() =
onView(withId(R.id.share_tab_favicon)).check(matches(ViewMatchers.isDisplayed())) onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
private fun assertShareBookmarkUrl() = private fun assertShareBookmarkUrl() =
onView(withId(R.id.share_tab_url)).check(matches(isDisplayed())) onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))

@ -57,14 +57,14 @@ class BrowserRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
sessionLoadedIdlingResource = SessionLoadedIdlingResource() sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")),
waitingTime
)
runWithIdleRes(sessionLoadedIdlingResource) { runWithIdleRes(sessionLoadedIdlingResource) {
onView(withId(R.id.mozac_browser_toolbar_url_view)) assertTrue(
.check(matches(withText(containsString(url.replace("http://", ""))))) mDevice.findObject(
UiSelector()
.resourceId("$packageName:id/mozac_browser_toolbar_url_view")
.textContains(url.replace("http://", ""))
).waitForExists(waitingTime)
)
} }
} }
@ -152,6 +152,8 @@ class BrowserRobot {
fun verifyNavURLBar() = assertNavURLBar() fun verifyNavURLBar() = assertNavURLBar()
fun verifyNavURLBarHidden() = assertNavURLBarHidden()
fun verifySecureConnectionLockIcon() = assertSecureConnectionLockIcon() fun verifySecureConnectionLockIcon() = assertSecureConnectionLockIcon()
fun verifyEnhancedTrackingProtectionSwitch() = assertEnhancedTrackingProtectionSwitch() fun verifyEnhancedTrackingProtectionSwitch() = assertEnhancedTrackingProtectionSwitch()
@ -192,6 +194,13 @@ class BrowserRobot {
) )
} }
fun verifyNotificationDotOnMainMenu() {
assertTrue(
mDevice.findObject(UiSelector().resourceId("$packageName:id/notification_dot"))
.waitForExists(waitingTime)
)
}
fun dismissContentContextMenu(containsURL: Uri) { fun dismissContentContextMenu(containsURL: Uri) {
onView(withText(containsURL.toString())) onView(withText(containsURL.toString()))
.inRoot(isDialog()) .inRoot(isDialog())
@ -297,6 +306,8 @@ class BrowserRobot {
fun createBookmark(url: Uri) { fun createBookmark(url: Uri) {
navigationToolbar { navigationToolbar {
}.enterURLAndEnterToBrowser(url) { }.enterURLAndEnterToBrowser(url) {
// needs to wait for the right url to load before saving a bookmark
verifyUrl(url.toString())
}.openThreeDotMenu { }.openThreeDotMenu {
clickAddBookmarkButton() clickAddBookmarkButton()
} }
@ -445,6 +456,15 @@ class BrowserRobot {
NotificationRobot().interact() NotificationRobot().interact()
return NotificationRobot.Transition() return NotificationRobot.Transition()
} }
fun goToHomescreen(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
openTabDrawer {
}.openNewTab {
}.dismissSearchBar {}
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
} }
} }
@ -460,11 +480,14 @@ fun dismissTrackingOnboarding() {
dismissOnboardingButton().click() dismissOnboardingButton().click()
} }
fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view)) fun navURLBar() = onView(withId(R.id.toolbar))
private fun assertNavURLBar() = navURLBar() private fun assertNavURLBar() = navURLBar()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNavURLBarHidden() = navURLBar()
.check(matches(not(isDisplayed())))
fun enhancedTrackingProtectionIndicator() = fun enhancedTrackingProtectionIndicator() =
onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator)) onView(withId(R.id.mozac_browser_toolbar_tracking_protection_indicator))

@ -15,11 +15,14 @@ import androidx.test.espresso.matcher.RootMatchers.isDialog
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers import org.hamcrest.CoreMatchers
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper import org.mozilla.fenix.helpers.TestHelper
@ -41,6 +44,23 @@ class DownloadRobot {
fun verifyPhotosAppOpens() = assertPhotosOpens() fun verifyPhotosAppOpens() = assertPhotosOpens()
fun verifyDownloadedFileName(fileName: String) {
mDevice.findObject(UiSelector().text(fileName)).waitForExists(waitingTime)
downloadedFile(fileName).check(matches(isDisplayed()))
}
fun verifyDownloadedFileIcon() = assertDownloadedFileIcon()
fun verifyEmptyDownloadsList() {
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/download_empty_view"))
.waitForExists(waitingTime)
onView(withText("No downloaded files")).check(matches(isDisplayed()))
}
fun waitForDownloadsListToExist() =
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/download_list"))
.waitForExists(waitingTime))
class Transition { class Transition {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition { fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {
clickDownloadButton().click() clickDownloadButton().click()
@ -85,6 +105,13 @@ class DownloadRobot {
DownloadRobot().interact() DownloadRobot().interact()
return Transition() return Transition()
} }
fun exitDownloadsManagerToBrowser(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
onView(withContentDescription("Navigate up")).click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
} }
} }
@ -126,3 +153,7 @@ private fun assertPhotosOpens() {
) )
} }
} }
private fun downloadedFile(fileName: String) = onView(withText(fileName))
private fun assertDownloadedFileIcon() = onView(withId(R.id.favicon)).check(matches(isDisplayed()))

@ -14,7 +14,6 @@ import androidx.test.espresso.NoMatchingViewException
import androidx.test.espresso.ViewInteraction import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.longClick
import androidx.test.espresso.action.ViewActions.swipeLeft import androidx.test.espresso.action.ViewActions.swipeLeft
import androidx.test.espresso.action.ViewActions.swipeRight import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
@ -38,8 +37,8 @@ import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.browser.state.state.searchEngines import mozilla.components.browser.state.state.searchEngines
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf import org.hamcrest.CoreMatchers.instanceOf
@ -48,7 +47,6 @@ import org.hamcrest.Matchers
import org.junit.Assert import org.junit.Assert
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
@ -74,7 +72,6 @@ class HomeScreenRobot {
fun verifyHomeMenu() = assertHomeMenu() fun verifyHomeMenu() = assertHomeMenu()
fun verifyTabButton() = assertTabButton() fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader() fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsHeader() = assertNoCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText() fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertHomeWordmark() fun verifyHomeWordmark() = assertHomeWordmark()
fun verifyHomeToolbar() = assertHomeToolbar() fun verifyHomeToolbar() = assertHomeToolbar()
@ -152,25 +149,18 @@ class HomeScreenRobot {
fun confirmDeleteCollection() { fun confirmDeleteCollection() {
onView(allOf(withText("DELETE"))).click() onView(allOf(withText("DELETE"))).click()
mDevice.waitNotNull(findObject(By.res("org.mozilla.fenix.debug:id/collections_header")), waitingTime) mDevice.waitNotNull(
} findObject(By.res("org.mozilla.fenix.debug:id/no_collections_header")),
waitingTime
fun typeCollectionName(name: String) { )
mDevice.wait(findObject(By.res("org.mozilla.fenix.debug:id/name_collection_edittext")), waitingTime)
collectionNameTextField().perform(ViewActions.replaceText(name))
collectionNameTextField().perform(ViewActions.pressImeActionButton())
mDevice.waitNotNull(Until.gone(text("Name collection")))
} }
fun saveTabsSelectedForCollection() = onView(withId(R.id.save_button)).click()
fun verifyCollectionIsDisplayed(title: String) { fun verifyCollectionIsDisplayed(title: String) {
mDevice.wait(findObject(text(title)), waitingTime) mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
collectionTitle(title).check(matches(isDisplayed())) collectionTitle(title).check(matches(isDisplayed()))
} }
fun verifyCollectionIcon() = fun verifyCollectionIcon() = onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
fun expandCollection(title: String) { fun expandCollection(title: String) {
try { try {
@ -190,9 +180,7 @@ class HomeScreenRobot {
} }
} }
fun clickSaveCollectionButton() = saveCollectionButton().click() fun verifyTabSavedInCollection(title: String, visible: Boolean = true) {
fun verifyItemInCollectionExists(title: String, visible: Boolean = true) {
try { try {
collectionItem(title) collectionItem(title)
.check( .check(
@ -203,11 +191,11 @@ class HomeScreenRobot {
} }
} }
fun verifyCollectionItemLogo() = fun verifyCollectionTabLogo() =
onView(withId(R.id.list_item_favicon)).check(matches(isDisplayed())) onView(withId(R.id.favicon)).check(matches(isDisplayed()))
fun verifyCollectionItemUrl() = fun verifyCollectionTabUrl() =
onView(withId(R.id.list_item_url)).check(matches(isDisplayed())) onView(withId(R.id.caption)).check(matches(isDisplayed()))
fun verifyShareCollectionButtonIsVisible(visible: Boolean) { fun verifyShareCollectionButtonIsVisible(visible: Boolean) {
shareCollectionButton() shareCollectionButton()
@ -233,54 +221,6 @@ class HomeScreenRobot {
) )
} }
fun verifySelectTabsView(vararg tabTitles: String) {
onView(allOf(withId(R.id.back_button), withText("Select Tabs")))
.check(matches(isDisplayed()))
for (title in tabTitles)
onView(withId(R.id.tab_list)).check(matches(hasItem(withText(title))))
}
fun verifyTabsSelectedCounterText(tabsSelected: Int) {
when (tabsSelected) {
0 -> onView(withId(R.id.bottom_bar_text)).check(matches(withText("Select tabs to save")))
1 -> onView(withId(R.id.bottom_bar_text)).check(matches(withText("1 tab selected")))
else -> onView(withId(R.id.bottom_bar_text)).check(matches(withText("$tabsSelected tabs selected")))
}
}
fun selectAllTabsForCollection() {
onView(withId(R.id.select_all_button))
.check(matches(withText("Select All")))
.click()
}
fun deselectAllTabsForCollection() {
onView(withId(R.id.select_all_button))
.check(matches(withText("Deselect All")))
.click()
}
fun selectTabForCollection(title: String) {
tab(title).click()
}
fun clickAddNewCollection() =
onView(allOf(withText("Add new collection"))).click()
fun verifyNameCollectionView() {
onView(allOf(withId(R.id.back_button), withText("Name collection")))
.check(matches(isDisplayed()))
}
fun verifyDefaultCollectionName(name: String) =
onView(withId(R.id.name_collection_edittext)).check(matches(withText(name)))
fun verifySelectCollectionView() {
onView(allOf(withId(R.id.back_button), withText("Select collection")))
.check(matches(isDisplayed()))
}
fun verifyShareTabsOverlay() = assertShareTabsOverlay() fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun clickShareCollectionButton() = onView(withId(R.id.collection_share_button)).click() fun clickShareCollectionButton() = onView(withId(R.id.collection_share_button)).click()
@ -303,22 +243,12 @@ class HomeScreenRobot {
} }
} }
fun longTapSelectTab(title: String) {
tab(title).perform(longClick())
}
fun goBackCollectionFlow() = collectionFlowBackButton().click()
fun scrollToElementByText(text: String): UiScrollable { fun scrollToElementByText(text: String): UiScrollable {
val appView = UiScrollable(UiSelector().scrollable(true)) val appView = UiScrollable(UiSelector().scrollable(true))
appView.scrollTextIntoView(text) appView.scrollTextIntoView(text)
return appView return appView
} }
fun closeTab() {
closeTabButton().click()
}
fun togglePrivateBrowsingModeOnOff() { fun togglePrivateBrowsingModeOnOff() {
onView(ViewMatchers.withResourceName("privateBrowsingButton")) onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click()) .perform(click())
@ -337,7 +267,7 @@ class HomeScreenRobot {
fun verifySnackBarText(expectedText: String) { fun verifySnackBarText(expectedText: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(findObject(By.text(expectedText)), TestAssetHelper.waitingTime) mDevice.waitNotNull(findObject(By.text(expectedText)), waitingTime)
} }
fun snackBarButtonClick(expectedText: String) { fun snackBarButtonClick(expectedText: String) {
@ -409,6 +339,22 @@ class HomeScreenRobot {
.perform(click()) .perform(click())
} }
fun triggerPrivateBrowsingShortcutPrompt(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
// Loop to press the PB icon for 5 times to display the Add the Private Browsing Shortcut CFR
for (i in 1..5) {
mDevice.findObject(UiSelector().resourceId("$packageName:id/privateBrowsingButton"))
.waitForExists(
waitingTime
)
onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click())
}
AddToHomeScreenRobot().interact()
return AddToHomeScreenRobot.Transition()
}
fun pressBack() { fun pressBack() {
onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack()) onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
} }
@ -501,12 +447,11 @@ class HomeScreenRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun clickSaveTabsToCollectionButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
mDevice.waitNotNull(findObject(text(title))) saveTabsToCollectionButton().click()
tab(title).click()
BrowserRobot().interact() TabDrawerRobot().interact()
return BrowserRobot.Transition() return TabDrawerRobot.Transition()
} }
} }
} }
@ -564,17 +509,14 @@ private fun assertCollectionsHeader() =
onView(allOf(withText("Collections"))) onView(allOf(withText("Collections")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNoCollectionsHeader() =
onView(allOf(withText("Collect the things that matter to you")))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNoCollectionsText() = private fun assertNoCollectionsText() =
onView( onView(
allOf( withText(
withText("Group together similar searches, sites, and tabs for quick access later.") containsString("Collect the things that matter to you.\n" +
"Group together similar searches, sites, and tabs for quick access later."
)
) )
) ).check(matches(isDisplayed()))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertHomeComponent() = private fun assertHomeComponent() =
onView(ViewMatchers.withResourceName("sessionControlRecyclerView")) onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
@ -752,9 +694,6 @@ private fun assertPrivateSessionMessage() =
private fun collectionThreeDotButton() = private fun collectionThreeDotButton() =
onView(allOf(withId(R.id.collection_overflow_button))) onView(allOf(withId(R.id.collection_overflow_button)))
private fun collectionNameTextField() =
onView(allOf(ViewMatchers.withResourceName("name_collection_edittext")))
private fun collectionTitle(title: String) = private fun collectionTitle(title: String) =
onView(allOf(withId(R.id.collection_title), withText(title))) onView(allOf(withId(R.id.collection_title), withText(title)))
@ -764,7 +703,7 @@ private fun assertExistingTopSitesList() =
private fun assertExistingTopSitesTabs(title: String) = private fun assertExistingTopSitesTabs(title: String) =
onView(allOf(withId(R.id.top_sites_list))) onView(allOf(withId(R.id.top_sites_list)))
.check(matches(hasItem(hasDescendant(withText(title))))) .check(matches(hasDescendant(withText(title))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNotExistingTopSitesList(title: String) = private fun assertNotExistingTopSitesList(title: String) =
@ -794,21 +733,20 @@ private fun assertShareTabsOverlay() {
private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button)) private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button))
private fun collectionItem(title: String) = private fun collectionItem(title: String) =
onView(allOf(withId(R.id.list_element_title), withText(title))) onView(allOf(withId(R.id.label), withText(title)))
private fun saveCollectionButton() = onView(withId(R.id.save_tab_group_button)) private fun saveTabsToCollectionButton() = onView(withId(R.id.add_tabs_to_collections_button))
private fun shareCollectionButton() = onView(withId(R.id.collection_share_button)) private fun shareCollectionButton() = onView(withId(R.id.collection_share_button))
private fun removeTabFromCollectionButton(title: String) = private fun removeTabFromCollectionButton(title: String) =
onView( onView(
allOf( allOf(
withId(R.id.list_item_action_button), withId(R.id.secondary_button),
hasSibling(withText(title)) hasSibling(withText(title))
) )
) )
private fun collectionFlowBackButton() = onView(withId(R.id.back_button))
private fun tabsCounter() = onView(withId(R.id.tab_button)) private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun tab(title: String) = private fun tab(title: String) =

@ -11,21 +11,27 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource import androidx.test.espresso.IdlingResource
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressImeActionButton import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withResourceName
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not import org.hamcrest.CoreMatchers.not
@ -53,8 +59,13 @@ class NavigationToolbarRobot {
fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems() fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems()
fun verifyReaderViewDetected(visible: Boolean = false): ViewInteraction =
assertReaderViewDetected(visible)
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm)) fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
fun toggleReaderView() = readerViewToggle().click()
class Transition { class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -98,8 +109,9 @@ class NavigationToolbarRobot {
runWithIdleRes(sessionLoadedIdlingResource) { runWithIdleRes(sessionLoadedIdlingResource) {
onView( onView(
anyOf( anyOf(
ViewMatchers.withResourceName("browserLayout"), withResourceName("browserLayout"),
ViewMatchers.withResourceName("onboarding_message") // Req ETP dialog withResourceName("onboarding_message"), // Req ETP dialog
withResourceName("download_button")
) )
) )
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
@ -272,6 +284,20 @@ private fun tabTrayButton() = onView(withId(R.id.tab_button))
private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard)) private fun fillLinkButton() = onView(withId(R.id.fill_link_from_clipboard))
private fun clearAddressBar() = onView(withId(R.id.mozac_browser_toolbar_clear_view)) private fun clearAddressBar() = onView(withId(R.id.mozac_browser_toolbar_clear_view))
private fun goBackButton() = mDevice.pressBack() private fun goBackButton() = mDevice.pressBack()
private fun readerViewToggle() =
onView(withParent(withId(R.id.mozac_browser_toolbar_page_actions)))
private fun assertReaderViewDetected(visible: Boolean) =
onView(
allOf(
withParent(withId(R.id.mozac_browser_toolbar_page_actions)),
withContentDescription("Reader view")
)
).check(
if (visible) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
else ViewAssertions.doesNotExist()
)
inline fun runWithIdleRes(ir: IdlingResource?, pendingCheck: () -> Unit) { inline fun runWithIdleRes(ir: IdlingResource?, pendingCheck: () -> Unit) {
try { try {
IdlingRegistry.getInstance().register(ir) IdlingRegistry.getInstance().register(ir)

@ -17,16 +17,12 @@ import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.nthChildOf
/** /**
* Implementation of Robot Pattern for Reader View UI. * Implementation of Robot Pattern for Reader View UI.
*/ */
class ReaderViewRobot { class ReaderViewRobot {
fun verifyReaderViewDetected(visible: Boolean = false): ViewInteraction =
assertReaderViewDetected(visible)
fun verifyAppearanceFontGroup(visible: Boolean = false): ViewInteraction = fun verifyAppearanceFontGroup(visible: Boolean = false): ViewInteraction =
assertAppearanceFontGroup(visible) assertAppearanceFontGroup(visible)
@ -184,18 +180,6 @@ fun readerViewRobot(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Trans
return ReaderViewRobot.Transition() return ReaderViewRobot.Transition()
} }
/**
* Detects for the blue notification dot in the three dot menu
*/
private fun assertReaderViewDetected(visible: Boolean) =
onView(
nthChildOf(
withId(R.id.mozac_browser_toolbar_menu), 2
)
).check(
matches(withEffectiveVisibility(visibleOrGone(visible)))
)
private fun assertAppearanceFontGroup(visible: Boolean) = private fun assertAppearanceFontGroup(visible: Boolean) =
onView( onView(
withId(R.id.mozac_feature_readerview_font_group) withId(R.id.mozac_feature_readerview_font_group)

@ -0,0 +1,222 @@
/* 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 android.net.Uri
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.Visibility
import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withParent
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.UiSelector
import org.hamcrest.Matchers
import org.hamcrest.Matchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.click
/**
* Implementation of Robot Pattern for the recently closed tabs menu.
*/
class RecentlyClosedTabsRobot {
fun waitForListToExist() =
mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/recently_closed_list"))
.waitForExists(
TestAssetHelper.waitingTime
)
fun verifyRecentlyClosedTabsMenuView() = assertRecentlyClosedTabsMenuView()
fun verifyEmptyRecentlyClosedTabsList() = assertEmptyRecentlyClosedTabsList()
fun verifyRecentlyClosedTabsPageTitle(title: String) = assertRecentlyClosedTabsPageTitle(title)
fun verifyRecentlyClosedTabsUrl(expectedUrl: Uri) = assertPageUrl(expectedUrl)
fun openRecentlyClosedTabsThreeDotMenu() = recentlyClosedTabsThreeDotButton().click()
fun verifyRecentlyClosedTabsMenuCopy() = assertRecentlyClosedTabsMenuCopy()
fun verifyRecentlyClosedTabsMenuShare() = assertRecentlyClosedTabsMenuShare()
fun verifyRecentlyClosedTabsMenuNewTab() = assertRecentlyClosedTabsOverlayNewTab()
fun verifyRecentlyClosedTabsMenuPrivateTab() = assertRecentlyClosedTabsMenuPrivateTab()
fun verifyRecentlyClosedTabsMenuDelete() = assertRecentlyClosedTabsMenuDelete()
fun clickCopyRecentlyClosedTabs() = recentlyClosedTabsCopyButton().click()
fun clickShareRecentlyClosedTabs() = recentlyClosedTabsShareButton().click()
fun clickDeleteCopyRecentlyClosedTabs() = recentlyClosedTabsDeleteButton().click()
fun verifyCopyRecentlyClosedTabsSnackBarText() = assertCopySnackBarText()
fun verifyShareOverlay() = assertRecentlyClosedShareOverlay()
fun verifyShareTabFavicon() = assertRecentlyClosedShareFavicon()
fun verifyShareTabTitle(title: String) = assetRecentlyClosedShareTitle(title)
fun verifyShareTabUrl(expectedUrl: Uri) = assertRecentlyClosedShareUrl(expectedUrl)
class Transition {
fun clickOpenInNewTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
recentlyClosedTabsNewTabButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun clickOpenInPrivateTab(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
recentlyClosedTabsNewPrivateTabButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
}
}
private fun assertRecentlyClosedTabsMenuView() {
onView(
allOf(
withText("Recently closed tabs"),
withParent(withId(R.id.navigationToolbar))
)
)
.check(
matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun assertEmptyRecentlyClosedTabsList() =
onView(
allOf(
withId(R.id.recently_closed_empty_view),
withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)
)
)
.check(
matches(withText("No recently closed tabs here")))
private fun assertPageUrl(expectedUrl: Uri) = onView(
allOf(
withId(R.id.url),
withEffectiveVisibility(
Visibility.VISIBLE
)
)
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString()))))
private fun recentlyClosedTabsPageTitle() = onView(
allOf(
withId(R.id.title),
withText("Test_Page_1")
)
)
private fun assertRecentlyClosedTabsPageTitle(title: String) {
recentlyClosedTabsPageTitle()
.check(
matches(withEffectiveVisibility(Visibility.VISIBLE)))
.check(
matches(withText(title)))
}
private fun recentlyClosedTabsThreeDotButton() =
onView(
allOf(
withId(R.id.overflow_menu),
withEffectiveVisibility(
Visibility.VISIBLE
)
)
)
private fun assertRecentlyClosedTabsMenuCopy() =
onView(withText("Copy"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertRecentlyClosedTabsMenuShare() =
onView(withText("Share"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertRecentlyClosedTabsOverlayNewTab() =
onView(withText("Open in new tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE))
)
private fun assertRecentlyClosedTabsMenuPrivateTab() =
onView(withText("Open in private tab"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun assertRecentlyClosedTabsMenuDelete() =
onView(withText("Delete"))
.check(
matches(
withEffectiveVisibility(Visibility.VISIBLE)
)
)
private fun recentlyClosedTabsCopyButton() = onView(withText("Copy"))
private fun copySnackBarText() = onView(withId(R.id.snackbar_text))
private fun assertCopySnackBarText() = copySnackBarText()
.check(
matches
(withText("URL copied")))
private fun recentlyClosedTabsShareButton() = onView(withText("Share"))
private fun assertRecentlyClosedShareOverlay() =
onView(withId(R.id.shareWrapper))
.check(
matches(ViewMatchers.isDisplayed()))
private fun assetRecentlyClosedShareTitle(title: String) =
onView(withId(R.id.share_tab_title))
.check(
matches(ViewMatchers.isDisplayed()))
.check(
matches(withText(title)))
private fun assertRecentlyClosedShareFavicon() =
onView(withId(R.id.share_tab_favicon))
.check(
matches(ViewMatchers.isDisplayed()))
private fun assertRecentlyClosedShareUrl(expectedUrl: Uri) =
onView(
allOf(
withId(R.id.share_tab_url),
withEffectiveVisibility(Visibility.VISIBLE)
)
)
.check(
matches(withText(Matchers.containsString(expectedUrl.toString()))))
private fun recentlyClosedTabsNewTabButton() = onView(withText("Open in new tab"))
private fun recentlyClosedTabsNewPrivateTabButton() = onView(withText("Open in private tab"))
private fun recentlyClosedTabsDeleteButton() = onView(withText("Delete"))

@ -38,7 +38,9 @@ class SettingsSubMenuDataCollectionRobot {
verifyDataCollectionOptions() verifyDataCollectionOptions()
verifyUsageAndTechnicalDataSwitchDefault() verifyUsageAndTechnicalDataSwitchDefault()
verifyMarketingDataSwitchDefault() verifyMarketingDataSwitchDefault()
verifyExperimentsSwitchDefault() // Temporarily disabled until https://github.com/mozilla-mobile/fenix/issues/17086 and
// https://github.com/mozilla-mobile/fenix/issues/17143 are resolved:
// verifyExperimentsSwitchDefault()
} }
class Transition { class Transition {
@ -80,8 +82,10 @@ private fun assertDataCollectionOptions() {
onView(withText(marketingDataText)) onView(withText(marketingDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
onView(withText(R.string.preference_experiments_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) // Temporarily disabled until https://github.com/mozilla-mobile/fenix/issues/17086 and
onView(withText(R.string.preference_experiments_summary_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) // https://github.com/mozilla-mobile/fenix/issues/17143 are resolved:
// onView(withText(R.string.preference_experiments_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
// onView(withText(R.string.preference_experiments_summary_2)).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun usageAndTechnicalDataButton() = onView(withText(R.string.preference_usage_data)) private fun usageAndTechnicalDataButton() = onView(withText(R.string.preference_usage_data))

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

@ -17,6 +17,7 @@ import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions import androidx.test.espresso.contrib.RecyclerViewActions
@ -30,6 +31,7 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.text import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.UiDevice import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject import androidx.test.uiautomator.Until.findObject
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -50,6 +52,19 @@ import org.mozilla.fenix.helpers.matchers.BottomSheetBehaviorStateMatcher
* Implementation of Robot Pattern for the home screen menu. * Implementation of Robot Pattern for the home screen menu.
*/ */
class TabDrawerRobot { class TabDrawerRobot {
fun verifyBrowserTabsTrayURL(url: String) {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
Until.findObject(By.res("org.mozilla.fenix.debug:id/mozac_browser_tabstray_url")),
waitingTime
)
onView(withId(R.id.mozac_browser_tabstray_url))
.check(matches(withText(containsString(url))))
}
fun verifyNormalBrowsingButtonIsDisplayed() = assertNormalBrowsingButton()
fun verifyExistingOpenTabs(title: String) = assertExistingOpenTabs(title) fun verifyExistingOpenTabs(title: String) = assertExistingOpenTabs(title)
fun verifyCloseTabsButton(title: String) = assertCloseTabsButton(title) fun verifyCloseTabsButton(title: String) = assertCloseTabsButton(title)
@ -64,8 +79,12 @@ class TabDrawerRobot {
fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist() fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist()
fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio() fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio()
fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState) fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState)
fun verifyOpenedTabThumbnail() = assertTabThumbnail()
fun closeTab() { fun closeTab() {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
).waitForExists(waitingTime)
closeTabButton().click() closeTabButton().click()
} }
@ -91,6 +110,9 @@ class TabDrawerRobot {
} }
fun snackBarButtonClick(expectedText: String) { fun snackBarButtonClick(expectedText: String) {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/snackbar_btn")
).waitForExists(waitingTime)
onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check( onView(allOf(withId(R.id.snackbar_btn), withText(expectedText))).check(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)) matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
).perform(click()) ).perform(click())
@ -111,6 +133,32 @@ class TabDrawerRobot {
fun clickTabMediaControlButton() = tabMediaControlButton().click() fun clickTabMediaControlButton() = tabMediaControlButton().click()
fun clickSelectTabs() = onView(withText("Select tabs")).click()
fun clickAddNewCollection() = addNewCollectionButton().click()
fun selectTab(title: String) = tab(title).click()
fun clickSaveCollection() = saveTabsToCollectionButton().click()
fun typeCollectionName(collectionName: String) {
collectionNameTextField().perform(replaceText(collectionName))
mDevice.findObject(UiSelector().textContains("OK")).click()
}
fun createCollection(
tabTitle: String,
collectionName: String,
firstCollection: Boolean = true
) {
clickSelectTabs()
selectTab(tabTitle)
clickSaveCollection()
if (!firstCollection)
clickAddNewCollection()
typeCollectionName(collectionName)
}
class Transition { class Transition {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -123,7 +171,8 @@ class TabDrawerRobot {
} }
fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition { fun openTabDrawer(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
org.mozilla.fenix.ui.robots.mDevice.waitForIdle() mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/tab_button"))
.waitForExists(waitingTime)
tabsCounter().click() tabsCounter().click()
@ -226,6 +275,24 @@ class TabDrawerRobot {
} }
return Transition() return Transition()
} }
fun openRecentlyClosedTabs(interact: RecentlyClosedTabsRobot.() -> Unit):
RecentlyClosedTabsRobot.Transition {
threeDotMenu().click()
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
mDevice.waitNotNull(
Until.findObject(text("Recently closed tabs")),
waitingTime
)
val menuRecentlyClosedTabs = mDevice.findObject(text("Recently closed tabs"))
menuRecentlyClosedTabs.click()
RecentlyClosedTabsRobot().interact()
return RecentlyClosedTabsRobot.Transition()
}
} }
} }
@ -311,6 +378,15 @@ private fun assertBehaviorState(expectedState: Int) {
.check(matches(BottomSheetBehaviorStateMatcher(expectedState))) .check(matches(BottomSheetBehaviorStateMatcher(expectedState)))
} }
private fun assertNormalBrowsingButton() {
normalBrowsingButton().check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun assertTabThumbnail() {
onView(withId(R.id.mozac_browser_tabstray_thumbnail))
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
}
private fun tab(title: String) = private fun tab(title: String) =
onView( onView(
allOf( allOf(
@ -323,3 +399,9 @@ private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun visibleOrGone(visibility: Boolean) = private fun visibleOrGone(visibility: Boolean) =
if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE if (visibility) ViewMatchers.Visibility.VISIBLE else ViewMatchers.Visibility.GONE
private fun addNewCollectionButton() = onView(withId(R.id.add_new_collection))
private fun saveTabsToCollectionButton() = onView(withId(R.id.collect_multi_select))
private fun collectionNameTextField() = onView(withId(R.id.collection_name))

@ -37,6 +37,7 @@ import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until import androidx.test.uiautomator.Until
import org.hamcrest.Matcher import org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click import org.mozilla.fenix.helpers.click
@ -47,6 +48,10 @@ import org.mozilla.fenix.share.ShareFragment
* Implementation of Robot Pattern for the three dot (main) menu. * Implementation of Robot Pattern for the three dot (main) menu.
*/ */
class ThreeDotMenuMainRobot { class ThreeDotMenuMainRobot {
fun verifyTabSettingsButton() = assertTabSettingsButton()
fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton()
fun verifyShareAllTabsButton() = assertShareAllTabsButton()
fun clickShareAllTabsButton() = shareAllTabsButton().click()
fun verifySettingsButton() = assertSettingsButton() fun verifySettingsButton() = assertSettingsButton()
fun verifyAddOnsButton() = assertAddOnsButton() fun verifyAddOnsButton() = assertAddOnsButton()
fun verifyHistoryButton() = assertHistoryButton() fun verifyHistoryButton() = assertHistoryButton()
@ -60,8 +65,8 @@ class ThreeDotMenuMainRobot {
fun verifyRefreshButton() = assertRefreshButton() fun verifyRefreshButton() = assertRefreshButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton() fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyShareButton() = assertShareButton() fun verifyShareButton() = assertShareButton()
fun verifyReaderViewToggle(visible: Boolean) = assertReaderViewToggle(visible)
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible) fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
fun clickShareButton() { fun clickShareButton() {
shareButton().click() shareButton().click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
@ -71,7 +76,7 @@ class ThreeDotMenuMainRobot {
fun verifySaveCollection() = assertSaveCollectionButton() fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton() fun verifySelectTabs() = assertSelectTabsButton()
fun clickBrowserViewSaveCollectionButton() { fun clickSaveCollectionButton() {
browserViewSaveCollectionButton().click() browserViewSaveCollectionButton().click()
} }
@ -116,6 +121,7 @@ class ThreeDotMenuMainRobot {
fun verifyAddToMobileHome() = assertAddToMobileHome() fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite() fun verifyDesktopSite() = assertDesktopSite()
fun verifyDownloadsButton() = assertDownloadsButton() fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifyThreeDotMainMenuItems() { fun verifyThreeDotMainMenuItems() {
verifyAddOnsButton() verifyAddOnsButton()
@ -135,6 +141,13 @@ class ThreeDotMenuMainRobot {
verifyRefreshButton() verifyRefreshButton()
} }
private fun assertShareTabsOverlay() {
onView(withId(R.id.shared_site_list)).check(matches(isDisplayed()))
onView(withId(R.id.share_tab_title)).check(matches(isDisplayed()))
onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))
}
class Transition { class Transition {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -150,6 +163,14 @@ class ThreeDotMenuMainRobot {
return SettingsRobot.Transition() return SettingsRobot.Transition()
} }
fun openDownloadsManager(interact: DownloadRobot.() -> Unit): DownloadRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
downloadsButton().click()
DownloadRobot().interact()
return DownloadRobot.Transition()
}
fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition { fun openSyncedTabs(interact: SyncedTabsRobot.() -> Unit): SyncedTabsRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime)
@ -160,9 +181,11 @@ class ThreeDotMenuMainRobot {
} }
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition { fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click() bookmarksButton().click()
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/bookmark_list")).waitForExists(waitingTime))
BookmarksRobot().interact() BookmarksRobot().interact()
return BookmarksRobot.Transition() return BookmarksRobot.Transition()
@ -251,6 +274,13 @@ class ThreeDotMenuMainRobot {
return HomeScreenRobot.Transition() return HomeScreenRobot.Transition()
} }
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
reportSiteIssueButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition { fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown()) onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
@ -288,13 +318,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun toggleReaderView(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
readerViewToggle().click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
}
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition { fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
readerViewAppearanceToggle().click() readerViewAppearanceToggle().click()
@ -317,6 +340,13 @@ class ThreeDotMenuMainRobot {
return AddToHomeScreenRobot.Transition() return AddToHomeScreenRobot.Transition()
} }
fun clickInstall(interact: AddToHomeScreenRobot.() -> Unit): AddToHomeScreenRobot.Transition {
installPWAButton().click()
AddToHomeScreenRobot().interact()
return AddToHomeScreenRobot.Transition()
}
fun selectExistingCollection(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition { fun selectExistingCollection(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime) mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime)
onView(withText(title)).click() onView(withText(title)).click()
@ -325,13 +355,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition() return BrowserRobot.Transition()
} }
fun clickOpenTabsMenuSaveCollection(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
saveCollectionButton().click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition { fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime) mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click() saveCollectionButton().click()
@ -444,6 +467,8 @@ private fun collectionNameTextField() =
private fun assertCollectionNameTextField() = collectionNameTextField() private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE))) .check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun findInPageButton() = onView(allOf(withText("Find in page"))) private fun findInPageButton() = onView(allOf(withText("Find in page")))
private fun assertFindInPageButton() = findInPageButton() private fun assertFindInPageButton() = findInPageButton()
@ -474,12 +499,6 @@ private fun assertWhatsNewButton() = whatsNewButton()
private fun addToHomeScreenButton() = onView(withText("Add to Home screen")) private fun addToHomeScreenButton() = onView(withText("Add to Home screen"))
private fun readerViewToggle() = onView(allOf(withText(R.string.browser_menu_read)))
private fun assertReaderViewToggle(visible: Boolean) = readerViewToggle()
.check(
if (visible) matches(withEffectiveVisibility(Visibility.VISIBLE)) else ViewAssertions.doesNotExist()
)
private fun readerViewAppearanceToggle() = private fun readerViewAppearanceToggle() =
onView(allOf(withText(R.string.browser_menu_read_appearance))) onView(allOf(withText(R.string.browser_menu_read_appearance)))
@ -511,6 +530,8 @@ private fun assertAddToMobileHome() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE))) ).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
} }
private fun installPWAButton() = onView(allOf(withId(R.id.highlight_text), withText("Install")))
private fun desktopSiteButton() = private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site))) onView(allOf(withText(R.string.browser_menu_desktop_site)))
private fun assertDesktopSite() { private fun assertDesktopSite() {
@ -530,3 +551,30 @@ private fun clickAddonsManagerButton() {
} }
private fun exitSaveCollectionButton() = onView(withId(R.id.back_button)).check(matches(isDisplayed())) private fun exitSaveCollectionButton() = onView(withId(R.id.back_button)).check(matches(isDisplayed()))
private fun tabSettingsButton() =
onView(allOf(withText("Tab settings"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertTabSettingsButton() {
tabSettingsButton()
.check(
matches(isDisplayed()))
}
private fun recentlyClosedTabsButton() =
onView(allOf(withText("Recently closed tabs"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertRecentlyClosedTabsButton() {
recentlyClosedTabsButton()
.check(
matches(isDisplayed()))
}
private fun shareAllTabsButton() =
onView(allOf(withText("Share all tabs"))).inRoot(RootMatchers.isPlatformPopup())
private fun assertShareAllTabsButton() {
shareAllTabsButton()
.check(
matches(isDisplayed()))
}

@ -168,7 +168,7 @@ class AppRequestInterceptor(
companion object { companion object {
internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html" internal const val LOW_AND_MEDIUM_RISK_ERROR_PAGES = "low_and_medium_risk_error_pages.html"
internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html" internal const val HIGH_RISK_ERROR_PAGES = "high_risk_error_pages.html"
internal const val AMO_BASE_URL = "https://addons.mozilla.org" internal const val AMO_BASE_URL = BuildConfig.AMO_BASE_URL
internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)" internal const val AMO_INSTALL_URL_REGEX = "$AMO_BASE_URL/android/downloads/file/([^\\s]+)/([^\\s]+\\.xpi)"
} }
} }

@ -21,29 +21,27 @@ object FeatureFlags {
*/ */
val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug val syncedTabsInTabsTray = Config.channel.isNightlyOrDebug
/**
* Enables downloads with external download managers.
*/
const val externalDownloadManager = true
/**
* Enables ETP cookie purging
*/
val etpCookiePurging = Config.channel.isNightlyOrDebug
/** /**
* Enables the Nimbus experiments library, especially the settings toggle to opt-out of * Enables the Nimbus experiments library, especially the settings toggle to opt-out of
* all experiments. * all experiments.
*/ */
val nimbusExperiments = Config.channel.isNightlyOrDebug // IMPORTANT: Only turn this back on once the following issues are resolved:
// - https://github.com/mozilla-mobile/fenix/issues/17086: Calls to
// getExperimentBranch seem to block on updateExperiments causing a
// large performance regression loading the home screen.
// - https://github.com/mozilla-mobile/fenix/issues/17143: Despite
// having wrapped getExperimentBranch/withExperiments in a catch-all
// users are still experiencing crashes.
const val nimbusExperiments = false
/** /**
* Enables the new MediaSession API. * Enables the new MediaSession API.
*/ */
val newMediaSessionApi = Config.channel.isNightlyOrDebug @Suppress("MayBeConst")
val newMediaSessionApi = true
/** /**
* Enabled showing site permission indicators in the toolbars. * Enables experimental WebAuthn support. This implementation should never reach release!
*/ */
val permissionIndicatorsToolbar = Config.channel.isNightlyOrDebug val webAuthFeature = Config.channel.isNightlyOrDebug
} }

@ -22,6 +22,7 @@ import kotlinx.coroutines.launch
import mozilla.appservices.Megazord import mozilla.appservices.Megazord
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.base.crash.Breadcrumb import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.push.PushProcessor import mozilla.components.concept.push.PushProcessor
import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider import mozilla.components.feature.addons.update.GlobalAddonDependencyProvider
@ -41,14 +42,15 @@ import mozilla.components.support.webextensions.WebExtensionSupport
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
import org.mozilla.fenix.components.metrics.MetricServiceType import org.mozilla.fenix.components.metrics.MetricServiceType
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.runBlockingIncrement import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.BrowsersCache import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
/** /**
*The main application class for Fenix. Records data to measure initialization performance. *The main application class for Fenix. Records data to measure initialization performance.
@ -130,7 +132,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.core.engine.warmUp() components.core.engine.warmUp()
} }
initializeWebExtensionSupport() initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads() restoreDownloads()
// Just to make sure it is impossible for any application-services pieces // Just to make sure it is impossible for any application-services pieces
@ -159,6 +161,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
components.appStartupTelemetry.onFenixApplicationOnCreate() components.appStartupTelemetry.onFenixApplicationOnCreate()
} }
private fun restoreBrowserState() = GlobalScope.launch(Dispatchers.Main) {
val store = components.core.store
val sessionStorage = components.core.sessionStorage
components.useCases.tabsUseCases.restore(sessionStorage, settings().getTabTimeout())
// Now that we have restored our previous state (if there's one) let's setup auto saving the state while
// the app is used.
sessionStorage.autoSave(store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
}
private fun restoreDownloads() { private fun restoreDownloads() {
components.useCases.downloadUseCases.restoreDownloads() components.useCases.downloadUseCases.restoreDownloads()
} }
@ -398,7 +414,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
onNewTabOverride = { onNewTabOverride = {
_, engineSession, url -> _, engineSession, url ->
val shouldCreatePrivateSession = val shouldCreatePrivateSession =
components.core.sessionManager.selectedSession?.private components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab ?: components.settings.openLinksInAPrivateTab
val session = Session(url, shouldCreatePrivateSession) val session = Session(url, shouldCreatePrivateSession)
@ -409,9 +425,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
_, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId) _, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)
}, },
onSelectTabOverride = { onSelectTabOverride = {
_, sessionId -> _, sessionId -> components.useCases.tabsUseCases.selectTab(sessionId)
val selected = components.core.sessionManager.findSessionById(sessionId)
selected?.let { components.useCases.tabsUseCases.selectTab(it) }
}, },
onExtensionsLoaded = { extensions -> onExtensionsLoaded = { extensions ->
components.addonUpdater.registerForFutureUpdates(extensions) components.addonUpdater.registerForFutureUpdates(extensions)

@ -14,6 +14,7 @@ import android.os.SystemClock
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.AttributeSet import android.util.AttributeSet
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewConfiguration import android.view.ViewConfiguration
import android.view.WindowManager import android.view.WindowManager
@ -89,6 +90,7 @@ import org.mozilla.fenix.library.bookmarks.DesktopFolders
import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.library.history.HistoryFragmentDirections
import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections import org.mozilla.fenix.library.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchDialogFragmentDirections import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService import org.mozilla.fenix.session.PrivateNotificationService
@ -138,6 +140,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
WebExtensionPopupFeature(components.core.store, ::openPopup) WebExtensionPopupFeature(components.core.store, ::openPopup)
} }
private var inflater: LayoutInflater? = null
private val navHost by lazy { private val navHost by lazy {
supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment supportFragmentManager.findFragmentById(R.id.container) as NavHostFragment
} }
@ -824,6 +828,16 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
} }
} }
override fun getSystemService(name: String): Any? {
if (LAYOUT_INFLATER_SERVICE == name) {
if (inflater == null) {
inflater = PerformanceInflater(LayoutInflater.from(baseContext), this)
}
return inflater
}
return super.getSystemService(name)
}
protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager { protected open fun createBrowsingModeManager(initialMode: BrowsingMode): BrowsingModeManager {
return DefaultBrowsingModeManager(initialMode, components.settings) { newMode -> return DefaultBrowsingModeManager(initialMode, components.settings) { newMode ->
themeManager.currentTheme = newMode themeManager.currentTheme = newMode

@ -46,6 +46,7 @@ import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.theme.ThemeManager import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale import java.util.Locale
import java.lang.ref.WeakReference import java.lang.ref.WeakReference

@ -5,7 +5,6 @@
package org.mozilla.fenix.addons package org.mozilla.fenix.addons
import android.view.View import android.view.View
import androidx.fragment.app.Fragment
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
/** /**
@ -23,14 +22,3 @@ internal fun showSnackBar(view: View, text: String, duration: Int = FenixSnackba
.setText(text) .setText(text)
.show() .show()
} }
/**
* Run the [block] only if the [Fragment] is attached.
*
* @param block A callback to be executed if the container [Fragment] is attached.
*/
internal inline fun Fragment.runIfFragmentIsAttached(block: () -> Unit) {
context?.let {
block()
}
}

@ -25,6 +25,7 @@ import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
/** /**
* An activity to show the details of a installed add-on. * An activity to show the details of a installed add-on.

@ -15,8 +15,9 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.android.synthetic.main.fragment_browser.view.* import kotlinx.android.synthetic.main.fragment_browser.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.SessionState
import mozilla.components.browser.thumbnails.BrowserThumbnails import mozilla.components.browser.thumbnails.BrowserThumbnails
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
@ -47,78 +48,110 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler { class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>() private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val trackingProtectionOverlayObserver = ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
private var readerModeAvailable = false private var readerModeAvailable = false
private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null
private var pwaOnboardingObserver: PwaOnboardingObserver? = null private var pwaOnboardingObserver: PwaOnboardingObserver? = null
@Suppress("LongMethod") @Suppress("LongMethod")
override fun initializeUI(view: View): Session? { override fun initializeUI(view: View, tab: SessionState) {
super.initializeUI(view, tab)
val context = requireContext() val context = requireContext()
val components = context.components val components = context.components
return super.initializeUI(view)?.also { if (context.settings().isSwipeToolbarToSwitchTabsEnabled) {
if (context.settings().isSwipeToolbarToSwitchTabsEnabled) { gestureLayout.addGestureListener(
gestureLayout.addGestureListener( ToolbarGestureHandler(
ToolbarGestureHandler( activity = requireActivity(),
activity = requireActivity(), contentLayout = browserLayout,
contentLayout = browserLayout, tabPreview = tabPreview,
tabPreview = tabPreview, toolbarLayout = browserToolbarView.view,
toolbarLayout = browserToolbarView.view, store = components.core.store,
sessionManager = components.core.sessionManager sessionManager = components.core.sessionManager
)
)
}
val readerModeAction =
BrowserToolbar.ToggleButton(
image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
imageSelected =
AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
contentDescription = requireContext().getString(R.string.browser_menu_read),
contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close),
visible = {
readerModeAvailable
},
selected = getSessionById()?.let {
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
} ?: false,
listener = browserInteractor::onReaderModePressed
) )
)
}
browserToolbarView.view.addPageAction(readerModeAction) val readerModeAction =
BrowserToolbar.ToggleButton(
thumbnailsFeature.set( image = AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode)!!,
feature = BrowserThumbnails(context, view.engineView, components.core.store), imageSelected =
owner = this, AppCompatResources.getDrawable(requireContext(), R.drawable.ic_readermode_selected)!!,
view = view contentDescription = requireContext().getString(R.string.browser_menu_read),
contentDescriptionSelected = requireContext().getString(R.string.browser_menu_read_close),
visible = {
readerModeAvailable
},
selected = getSessionById()?.let {
activity?.components?.core?.store?.state?.findTab(it.id)?.readerState?.active
} ?: false,
listener = browserInteractor::onReaderModePressed
) )
readerViewFeature.set( browserToolbarView.view.addPageAction(readerModeAction)
feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
ReaderViewFeature(
context,
components.core.engine,
components.core.store,
view.readerViewControlsBar
) { available, active ->
if (available) {
components.analytics.metrics.track(Event.ReaderModeAvailable)
}
readerModeAvailable = available thumbnailsFeature.set(
readerModeAction.setSelected(active) feature = BrowserThumbnails(context, view.engineView, components.core.store),
safeInvalidateBrowserToolbarView() owner = this,
view = view
)
readerViewFeature.set(
feature = components.strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
ReaderViewFeature(
context,
components.core.engine,
components.core.store,
view.readerViewControlsBar
) { available, active ->
if (available) {
components.analytics.metrics.track(Event.ReaderModeAvailable)
} }
},
readerModeAvailable = available
readerModeAction.setSelected(active)
safeInvalidateBrowserToolbarView()
}
},
owner = this,
view = view
)
windowFeature.set(
feature = WindowFeature(
store = components.core.store,
tabsUseCases = components.useCases.tabsUseCases
),
owner = this,
view = view
)
if (context.settings().shouldShowOpenInAppCfr) {
openInAppOnboardingObserver.set(
feature = OpenInAppOnboardingObserver(
context = context,
store = context.components.core.store,
lifecycleOwner = this,
navController = findNavController(),
settings = context.settings(),
appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserLayout as ViewGroup
),
owner = this, owner = this,
view = view view = view
) )
}
windowFeature.set( if (context.settings().shouldShowTrackingProtectionCfr) {
feature = WindowFeature( trackingProtectionOverlayObserver.set(
store = components.core.store, feature = TrackingProtectionOverlay(
tabsUseCases = components.useCases.tabsUseCases context = context,
store = context.components.core.store,
lifecycleOwner = viewLifecycleOwner,
settings = context.settings(),
metrics = context.components.analytics.metrics,
getToolbar = { browserToolbarView.view }
), ),
owner = this, owner = this,
view = view view = view
@ -130,36 +163,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
super.onStart() super.onStart()
val context = requireContext() val context = requireContext()
val settings = context.settings() val settings = context.settings()
val session = getSessionById()
val toolbarSessionObserver = TrackingProtectionOverlay(
context = context,
settings = settings,
metrics = context.components.analytics.metrics
) {
browserToolbarView.view
}
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16945
session?.register(toolbarSessionObserver, viewLifecycleOwner, autoPause = true)
if (settings.shouldShowOpenInAppCfr && session != null) {
openInAppOnboardingObserver = OpenInAppOnboardingObserver(
context = context,
navController = findNavController(),
settings = settings,
appLinksUseCases = context.components.useCases.appLinksUseCases,
container = browserLayout as ViewGroup
)
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16949
session.register(
openInAppOnboardingObserver!!,
owner = this,
autoPause = true
)
}
if (!settings.userKnowsAboutPwas) { if (!settings.userKnowsAboutPwas) {
pwaOnboardingObserver = PwaOnboardingObserver( pwaOnboardingObserver = PwaOnboardingObserver(
@ -178,14 +181,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
// This observer initialized in onStart has a reference to fragment's view.
// Prevent it leaking the view after the latter onDestroyView.
if (openInAppOnboardingObserver != null) {
@Suppress("DEPRECATION")
// TODO Use browser store instead of session observer: https://github.com/mozilla-mobile/fenix/issues/16949
getSessionById()?.unregister(openInAppOnboardingObserver!!)
openInAppOnboardingObserver = null
}
pwaOnboardingObserver?.stop() pwaOnboardingObserver?.stop()
} }
@ -208,29 +203,30 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
return readerViewFeature.onBackPressed() || super.onBackPressed() return readerViewFeature.onBackPressed() || super.onBackPressed()
} }
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) { override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val directions = val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment( BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = session.id, sessionId = tab.id,
url = session.url, url = tab.content.url,
title = session.title, title = tab.content.title,
isSecured = session.securityInfo.secure, isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions, sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(), gravity = getAppropriateLayoutGravity(),
certificateName = session.securityInfo.issuer certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights
) )
nav(R.id.browserFragment, directions) nav(R.id.browserFragment, directions)
} }
override fun navToTrackingProtectionPanel(session: Session) { override fun navToTrackingProtectionPanel(tab: SessionState) {
val navController = findNavController() val navController = findNavController()
requireComponents.useCases.trackingProtectionUseCases.containsException(session.id) { contains -> requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
val isEnabled = session.trackerBlockingEnabled && !contains val isEnabled = tab.trackingProtection.enabled && !contains
val directions = val directions =
BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment( BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
sessionId = session.id, sessionId = tab.id,
url = session.url, url = tab.content.url,
trackingProtectionEnabled = isEnabled, trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity() gravity = getAppropriateLayoutGravity()
) )
@ -239,11 +235,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
} }
private val collectionStorageObserver = object : TabCollectionStorage.Observer { private val collectionStorageObserver = object : TabCollectionStorage.Observer {
override fun onCollectionCreated(title: String, sessions: List<Session>, id: Long?) { override fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) {
showTabSavedToCollectionSnackbar(sessions.size, true) showTabSavedToCollectionSnackbar(sessions.size, true)
} }
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) { override fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) {
showTabSavedToCollectionSnackbar(sessions.size) showTabSavedToCollectionSnackbar(sessions.size)
} }

@ -7,10 +7,20 @@ package org.mozilla.fenix.browser
import android.content.Context import android.content.Context
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.session.Session import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.mapNotNull
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.app.links.AppLinksUseCases import mozilla.components.feature.app.links.AppLinksUseCases
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@ -18,51 +28,79 @@ import org.mozilla.fenix.utils.Settings
/** /**
* Displays an [InfoBanner] when a user visits a website that can be opened in an installed native app. * Displays an [InfoBanner] when a user visits a website that can be opened in an installed native app.
*/ */
@ExperimentalCoroutinesApi
@Suppress("LongParameterList")
class OpenInAppOnboardingObserver( class OpenInAppOnboardingObserver(
private val context: Context, private val context: Context,
private val store: BrowserStore,
private val lifecycleOwner: LifecycleOwner,
private val navController: NavController, private val navController: NavController,
private val settings: Settings, private val settings: Settings,
private val appLinksUseCases: AppLinksUseCases, private val appLinksUseCases: AppLinksUseCases,
private val container: ViewGroup private val container: ViewGroup
) : Session.Observer { ) : LifecycleAwareFeature {
private var scope: CoroutineScope? = null
private var currentUrl: String? = null
private var sessionDomainForDisplayedBanner: String? = null
@VisibleForTesting
internal var sessionDomainForDisplayedBanner: String? = null
@VisibleForTesting @VisibleForTesting
internal var infoBanner: InfoBanner? = null internal var infoBanner: InfoBanner? = null
override fun onUrlChanged(session: Session, url: String) { override fun start() {
sessionDomainForDisplayedBanner?.let { scope = store.flowScoped(lifecycleOwner) { flow ->
if (url.tryGetHostFromUrl() != it) { flow.mapNotNull { state ->
infoBanner?.dismiss() state.selectedTab
}
.ifAnyChanged {
tab -> arrayOf(tab.content.url, tab.content.loading)
}
.collect { tab ->
if (tab.content.url != currentUrl) {
sessionDomainForDisplayedBanner?.let {
if (tab.content.url.tryGetHostFromUrl() != it) {
infoBanner?.dismiss()
}
}
currentUrl = tab.content.url
} else {
// Loading state has changed
maybeShowOpenInAppBanner(tab.content.url, tab.content.loading)
}
} }
} }
} }
override fun onLoadingStateChanged(session: Session, loading: Boolean) { override fun stop() {
scope?.cancel()
}
private fun maybeShowOpenInAppBanner(url: String, loading: Boolean) {
if (loading || settings.openLinksInExternalApp || !settings.shouldShowOpenInAppCfr) { if (loading || settings.openLinksInExternalApp || !settings.shouldShowOpenInAppCfr) {
return return
} }
val appLink = appLinksUseCases.appLinkRedirect val appLink = appLinksUseCases.appLinkRedirect
if (appLink(url).hasExternalApp()) {
if (appLink(session.url).hasExternalApp()) { infoBanner = createInfoBanner()
infoBanner = InfoBanner(
context = context,
message = context.getString(R.string.open_in_app_cfr_info_message),
dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text),
actionText = context.getString(R.string.open_in_app_cfr_positive_button_text),
container = container
) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment(
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app)
)
navController.nav(R.id.browserFragment, directions)
}
infoBanner?.showBanner() infoBanner?.showBanner()
sessionDomainForDisplayedBanner = session.url.tryGetHostFromUrl() sessionDomainForDisplayedBanner = url.tryGetHostFromUrl()
settings.shouldShowOpenInAppBanner = false settings.shouldShowOpenInAppBanner = false
} }
} }
@VisibleForTesting
internal fun createInfoBanner(): InfoBanner {
return InfoBanner(
context = context,
message = context.getString(R.string.open_in_app_cfr_info_message),
dismissText = context.getString(R.string.open_in_app_cfr_negative_button_text),
actionText = context.getString(R.string.open_in_app_cfr_positive_button_text),
container = container
) {
val directions = BrowserFragmentDirections.actionBrowserFragmentToSettingsFragment(
preferenceToScrollTo = context.getString(R.string.pref_key_open_links_in_external_app)
)
navController.nav(R.id.browserFragment, directions)
}
}
} }

@ -19,6 +19,8 @@ import androidx.core.view.isVisible
import androidx.interpolator.view.animation.LinearOutSlowInInterpolator import androidx.interpolator.view.animation.LinearOutSlowInInterpolator
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.ktx.android.view.getRectWithViewLocation import mozilla.components.support.ktx.android.view.getRectWithViewLocation
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.getRectWithScreenLocation import org.mozilla.fenix.ext.getRectWithScreenLocation
@ -40,6 +42,7 @@ class ToolbarGestureHandler(
private val contentLayout: View, private val contentLayout: View,
private val tabPreview: TabPreview, private val tabPreview: TabPreview,
private val toolbarLayout: View, private val toolbarLayout: View,
private val store: BrowserStore,
private val sessionManager: SessionManager private val sessionManager: SessionManager
) : SwipeGestureListener { ) : SwipeGestureListener {
@ -145,15 +148,15 @@ class ToolbarGestureHandler(
private fun getDestination(): Destination { private fun getDestination(): Destination {
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
val currentSession = sessionManager.selectedSession ?: return Destination.None val currentTab = store.state.selectedTab ?: return Destination.None
val currentIndex = sessionManager.sessionsOfType(currentSession.private).indexOfFirst { val currentIndex = sessionManager.sessionsOfType(currentTab.content.private).indexOfFirst {
it.id == currentSession.id it.id == currentTab.id
} }
return if (currentIndex == -1) { return if (currentIndex == -1) {
Destination.None Destination.None
} else { } else {
val sessions = sessionManager.sessionsOfType(currentSession.private) val sessions = sessionManager.sessionsOfType(currentTab.content.private)
val index = when (gestureDirection) { val index = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) { GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
currentIndex + 1 currentIndex + 1

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

@ -9,14 +9,15 @@ package org.mozilla.fenix.collections
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import org.mozilla.fenix.components.TabCollectionStorage import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.getDefaultCollectionNumber import org.mozilla.fenix.ext.getDefaultCollectionNumber
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.home.Tab import org.mozilla.fenix.home.Tab
interface CollectionCreationController { interface CollectionCreationController {
@ -59,24 +60,24 @@ interface CollectionCreationController {
fun removeTabFromSelection(tab: Tab) fun removeTabFromSelection(tab: Tab)
} }
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> { fun List<Tab>.toTabSessionStateList(store: BrowserStore): List<TabSessionState> {
return this.mapNotNull { sessionManager.findSessionById(it.sessionId) } return this.mapNotNull { store.state.findTab(it.sessionId) }
} }
/** /**
* @param store Store used to hold in-memory collection state. * @param store Store used to hold in-memory collection state.
* @param browserStore The global `BrowserStore` instance.
* @param dismiss Callback to dismiss the collection creation dialog. * @param dismiss Callback to dismiss the collection creation dialog.
* @param metrics Controller that handles telemetry events. * @param metrics Controller that handles telemetry events.
* @param tabCollectionStorage Storage used to save tab collections to disk. * @param tabCollectionStorage Storage used to save tab collections to disk.
* @param sessionManager Used to query and serialize tabs.
* @param scope Coroutine scope to launch coroutines. * @param scope Coroutine scope to launch coroutines.
*/ */
class DefaultCollectionCreationController( class DefaultCollectionCreationController(
private val store: CollectionCreationStore, private val store: CollectionCreationStore,
private val browserStore: BrowserStore,
private val dismiss: () -> Unit, private val dismiss: () -> Unit,
private val metrics: MetricController, private val metrics: MetricController,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val sessionManager: SessionManager,
private val scope: CoroutineScope private val scope: CoroutineScope
) : CollectionCreationController { ) : CollectionCreationController {
@ -88,13 +89,13 @@ class DefaultCollectionCreationController(
override fun saveCollectionName(tabs: List<Tab>, name: String) { override fun saveCollectionName(tabs: List<Tab>, name: String) {
dismiss() dismiss()
val sessionBundle = tabs.toSessionBundle(sessionManager) val sessionBundle = tabs.toTabSessionStateList(browserStore)
scope.launch { scope.launch {
tabCollectionStorage.createCollection(name, sessionBundle) tabCollectionStorage.createCollection(name, sessionBundle)
} }
metrics.track( metrics.track(
Event.CollectionSaved(sessionManager.normalSessionSize(), sessionBundle.size) Event.CollectionSaved(browserStore.state.normalTabs.size, sessionBundle.size)
) )
} }
@ -129,14 +130,14 @@ class DefaultCollectionCreationController(
override fun selectCollection(collection: TabCollection, tabs: List<Tab>) { override fun selectCollection(collection: TabCollection, tabs: List<Tab>) {
dismiss() dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager) val sessionBundle = tabs.toList().toTabSessionStateList(browserStore)
scope.launch { scope.launch {
tabCollectionStorage tabCollectionStorage
.addTabsToCollection(collection, sessionBundle) .addTabsToCollection(collection, sessionBundle)
} }
metrics.track( metrics.track(
Event.CollectionTabsAdded(sessionManager.normalSessionSize(), sessionBundle.size) Event.CollectionTabsAdded(browserStore.state.normalTabs.size, sessionBundle.size)
) )
} }

@ -55,10 +55,10 @@ class CollectionCreationFragment : DialogFragment() {
collectionCreationInteractor = DefaultCollectionCreationInteractor( collectionCreationInteractor = DefaultCollectionCreationInteractor(
DefaultCollectionCreationController( DefaultCollectionCreationController(
collectionCreationStore, collectionCreationStore,
requireComponents.core.store,
::dismiss, ::dismiss,
requireComponents.analytics.metrics, requireComponents.analytics.metrics,
requireComponents.core.tabCollectionStorage, requireComponents.core.tabCollectionStorage,
requireComponents.core.sessionManager,
scope = lifecycleScope scope = lifecycleScope
) )
) )

@ -13,13 +13,16 @@ import mozilla.components.feature.addons.migration.DefaultSupportedAddonsChecker
import mozilla.components.feature.addons.migration.SupportedAddonsChecker import mozilla.components.feature.addons.migration.SupportedAddonsChecker
import mozilla.components.feature.addons.update.AddonUpdater import mozilla.components.feature.addons.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore import mozilla.components.support.migration.state.MigrationStore
import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.perf.StrictModeManager import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler import org.mozilla.fenix.utils.ClipboardHandler
@ -28,7 +31,7 @@ import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.wifi.WifiConnectionMonitor import org.mozilla.fenix.wifi.WifiConnectionMonitor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
private const val DAY_IN_MINUTES = 24 * 60L private const val AMO_COLLECTION_MAX_CACHE_AGE = 2 * 24 * 60L // Two days in minutes
/** /**
* Provides access to all components. This class is an implementation of the Service Locator * Provides access to all components. This class is an implementation of the Service Locator
@ -70,6 +73,7 @@ class Components(private val context: Context) {
core.store, core.store,
useCases.sessionUseCases, useCases.sessionUseCases,
useCases.tabsUseCases, useCases.tabsUseCases,
useCases.customTabsUseCases,
useCases.searchUseCases, useCases.searchUseCases,
core.relationChecker, core.relationChecker,
core.customTabsStore, core.customTabsStore,
@ -93,9 +97,10 @@ class Components(private val context: Context) {
PagedAddonCollectionProvider( PagedAddonCollectionProvider(
context, context,
core.client, core.client,
serverURL = BuildConfig.AMO_SERVER_URL,
collectionAccount = context.settings().customAddonsAccount, collectionAccount = context.settings().customAddonsAccount,
collectionName = context.settings().customAddonsCollection, collectionName = context.settings().customAddonsCollection,
maxCacheAgeInMinutes = DAY_IN_MINUTES maxCacheAgeInMinutes = AMO_COLLECTION_MAX_CACHE_AGE
) )
} }
} }
@ -113,7 +118,7 @@ class Components(private val context: Context) {
onNotificationClickIntent = Intent(context, HomeActivity::class.java).apply { onNotificationClickIntent = Intent(context, HomeActivity::class.java).apply {
action = Intent.ACTION_VIEW action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
data = "fenix://settings_addon_manager".toUri() data = "${BuildConfig.DEEP_LINK_SCHEME}://settings_addon_manager".toUri()
} }
) )
} }
@ -131,6 +136,10 @@ class Components(private val context: Context) {
addonCollectionProvider.setCollectionName(addonsCollection) addonCollectionProvider.setCollectionName(addonsCollection)
} }
val sitePermissionsStorage by lazyMonitored {
SitePermissionsStorage(context, context.components.core.engine)
}
val analytics by lazyMonitored { Analytics(context) } val analytics by lazyMonitored { Analytics(context) }
val publicSuffixList by lazyMonitored { PublicSuffixList(context) } val publicSuffixList by lazyMonitored { PublicSuffixList(context) }
val clipboardHandler by lazyMonitored { ClipboardHandler(context) } val clipboardHandler by lazyMonitored { ClipboardHandler(context) }

@ -11,10 +11,6 @@ import android.os.Build
import android.os.StrictMode import android.os.StrictMode
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import io.sentry.Sentry import io.sentry.Sentry
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import mozilla.components.browser.engine.gecko.GeckoEngine import mozilla.components.browser.engine.gecko.GeckoEngine
import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient import mozilla.components.browser.engine.gecko.fetch.GeckoViewFetchClient
import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.BrowserIcons
@ -23,7 +19,6 @@ import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.session.engine.EngineMiddleware import mozilla.components.browser.session.engine.EngineMiddleware
import mozilla.components.browser.session.storage.SessionStorage import mozilla.components.browser.session.storage.SessionStorage
import mozilla.components.browser.session.undo.UndoMiddleware import mozilla.components.browser.session.undo.UndoMiddleware
import mozilla.components.browser.state.action.RestoreCompleteAction
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.storage.sync.PlacesBookmarksStorage import mozilla.components.browser.storage.sync.PlacesBookmarksStorage
@ -39,6 +34,8 @@ import mozilla.components.concept.fetch.Client
import mozilla.components.feature.customtabs.store.CustomTabsServiceStore import mozilla.components.feature.customtabs.store.CustomTabsServiceStore
import mozilla.components.feature.downloads.DownloadMiddleware import mozilla.components.feature.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager import mozilla.components.feature.pwa.WebAppShortcutManager
@ -64,14 +61,17 @@ import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.TelemetryMiddleware import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.components.search.SearchMigration import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry import org.mozilla.fenix.search.telemetry.incontent.InContentTelemetry
@ -79,12 +79,6 @@ import org.mozilla.fenix.settings.SupportUtils
import org.mozilla.fenix.settings.advanced.getSelectedLocale import org.mozilla.fenix.settings.advanced.getSelectedLocale
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import org.mozilla.fenix.utils.getUndoDelay import org.mozilla.fenix.utils.getUndoDelay
import java.util.concurrent.TimeUnit
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService
/** /**
* Component group for all core browser functionality. * Component group for all core browser functionality.
@ -166,7 +160,7 @@ class Core(
) )
} }
private val sessionStorage: SessionStorage by lazyMonitored { val sessionStorage: SessionStorage by lazyMonitored {
SessionStorage(context, engine = engine) SessionStorage(context, engine = engine)
} }
@ -239,7 +233,7 @@ class Core(
* case all sessions/tabs are closed. * case all sessions/tabs are closed.
*/ */
val sessionManager by lazyMonitored { val sessionManager by lazyMonitored {
SessionManager(engine, store).also { sessionManager -> SessionManager(engine, store).also {
// Install the "icons" WebExtension to automatically load icons for every visited website. // Install the "icons" WebExtension to automatically load icons for every visited website.
icons.install(engine, store) icons.install(engine, store)
@ -249,40 +243,6 @@ class Core(
// Install the "cookies" WebExtension and tracks user interaction with SERPs. // Install the "cookies" WebExtension and tracks user interaction with SERPs.
searchTelemetry.install(engine, store) searchTelemetry.install(engine, store)
// Restore the previous state.
GlobalScope.launch(Dispatchers.Main) {
withContext(Dispatchers.IO) {
sessionStorage.restore()
}?.let { snapshot ->
sessionManager.restore(
snapshot,
updateSelection = (sessionManager.selectedSession == null)
)
}
// Now that we have restored our previous state (if there's one) let's setup auto saving the state while
// the app is used.
sessionStorage.autoSave(store)
.periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS)
.whenGoingToBackground()
.whenSessionsChange()
// Now that we have restored our previous state (if there's one) let's remove timed out tabs
if (!context.settings().manuallyCloseTabs) {
store.state.tabs.filter {
(System.currentTimeMillis() - it.lastAccess) > context.settings()
.getTabTimeout()
}.forEach {
val session = sessionManager.findSessionById(it.id)
if (session != null) {
sessionManager.remove(session)
}
}
}
store.dispatch(RestoreCompleteAction)
}
WebNotificationFeature( WebNotificationFeature(
context, engine, icons, R.drawable.ic_status_logo, context, engine, icons, R.drawable.ic_status_logo,
permissionStorage.permissionsStorage, HomeActivity::class.java permissionStorage.permissionsStorage, HomeActivity::class.java
@ -346,7 +306,6 @@ class Core(
val tabCollectionStorage by lazyMonitored { val tabCollectionStorage by lazyMonitored {
TabCollectionStorage( TabCollectionStorage(
context, context,
sessionManager,
strictMode strictMode
) )
} }

@ -15,6 +15,7 @@ import mozilla.components.feature.pwa.intent.TrustedWebActivityIntentProcessor
import mozilla.components.feature.pwa.intent.WebAppIntentProcessor import mozilla.components.feature.pwa.intent.WebAppIntentProcessor
import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.search.SearchUseCases
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.service.digitalassetlinks.RelationChecker import mozilla.components.service.digitalassetlinks.RelationChecker
import mozilla.components.support.migration.MigrationIntentProcessor import mozilla.components.support.migration.MigrationIntentProcessor
@ -35,6 +36,7 @@ class IntentProcessors(
private val store: BrowserStore, private val store: BrowserStore,
private val sessionUseCases: SessionUseCases, private val sessionUseCases: SessionUseCases,
private val tabsUseCases: TabsUseCases, private val tabsUseCases: TabsUseCases,
private val customTabsUseCases: CustomTabsUseCases,
private val searchUseCases: SearchUseCases, private val searchUseCases: SearchUseCases,
private val relationChecker: RelationChecker, private val relationChecker: RelationChecker,
private val customTabsStore: CustomTabsServiceStore, private val customTabsStore: CustomTabsServiceStore,
@ -45,22 +47,22 @@ class IntentProcessors(
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents.
*/ */
val intentProcessor by lazyMonitored { val intentProcessor by lazyMonitored {
TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = false) TabIntentProcessor(tabsUseCases, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = false)
} }
/** /**
* Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs. * Provides intent processing functionality for ACTION_VIEW and ACTION_SEND intents in private tabs.
*/ */
val privateIntentProcessor by lazyMonitored { val privateIntentProcessor by lazyMonitored {
TabIntentProcessor(sessionManager, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = true) TabIntentProcessor(tabsUseCases, sessionUseCases.loadUrl, searchUseCases.newTabSearch, isPrivate = true)
} }
val customTabIntentProcessor by lazyMonitored { val customTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = false) CustomTabIntentProcessor(customTabsUseCases.add, context.resources, isPrivate = false)
} }
val privateCustomTabIntentProcessor by lazyMonitored { val privateCustomTabIntentProcessor by lazyMonitored {
CustomTabIntentProcessor(sessionManager, sessionUseCases.loadUrl, context.resources, isPrivate = true) CustomTabIntentProcessor(customTabsUseCases.add, context.resources, isPrivate = true)
} }
val externalAppIntentProcessors by lazyMonitored { val externalAppIntentProcessors by lazyMonitored {

@ -5,46 +5,33 @@
package org.mozilla.fenix.components package org.mozilla.fenix.components
import android.content.Context import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.paging.DataSource import androidx.paging.DataSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissions.Status
import mozilla.components.feature.sitepermissions.SitePermissionsStorage import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.utils.Mockable import org.mozilla.fenix.utils.Mockable
import kotlin.coroutines.CoroutineContext
@Mockable @Mockable
class PermissionStorage(private val context: Context) { class PermissionStorage(
private val context: Context,
val permissionsStorage by lazy { @VisibleForTesting internal val dispatcher: CoroutineContext = Dispatchers.IO,
SitePermissionsStorage(context, context.components.core.engine) @VisibleForTesting internal val permissionsStorage: SitePermissionsStorage =
} context.components.sitePermissionsStorage
) {
fun addSitePermissionException(
origin: String, suspend fun add(sitePermissions: SitePermissions) = withContext(dispatcher) {
location: Status,
notification: Status,
microphone: Status,
camera: Status
): SitePermissions {
val sitePermissions = SitePermissions(
origin = origin,
location = location,
camera = camera,
microphone = microphone,
notification = notification,
savedAt = System.currentTimeMillis()
)
permissionsStorage.save(sitePermissions) permissionsStorage.save(sitePermissions)
return sitePermissions
} }
suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(Dispatchers.IO) { suspend fun findSitePermissionsBy(origin: String): SitePermissions? = withContext(dispatcher) {
permissionsStorage.findSitePermissionsBy(origin) permissionsStorage.findSitePermissionsBy(origin)
} }
suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(Dispatchers.IO) { suspend fun updateSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.update(sitePermissions) permissionsStorage.update(sitePermissions)
} }
@ -52,11 +39,11 @@ class PermissionStorage(private val context: Context) {
return permissionsStorage.getSitePermissionsPaged() return permissionsStorage.getSitePermissionsPaged()
} }
suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(Dispatchers.IO) { suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) {
permissionsStorage.remove(sitePermissions) permissionsStorage.remove(sitePermissions)
} }
suspend fun deleteAllSitePermissions() = withContext(Dispatchers.IO) { suspend fun deleteAllSitePermissions() = withContext(dispatcher) {
permissionsStorage.removeAll() permissionsStorage.removeAll()
} }
} }

@ -8,12 +8,10 @@ import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.paging.DataSource
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.session.SessionManager
import mozilla.components.feature.tab.collections.Tab import mozilla.components.feature.tab.collections.Tab
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.TabCollectionStorage import mozilla.components.feature.tab.collections.TabCollectionStorage
@ -28,7 +26,6 @@ import org.mozilla.fenix.utils.Mockable
@Mockable @Mockable
class TabCollectionStorage( class TabCollectionStorage(
private val context: Context, private val context: Context,
private val sessionManager: SessionManager,
strictMode: StrictModeManager, strictMode: StrictModeManager,
private val delegate: Observable<Observer> = ObserverRegistry() private val delegate: Observable<Observer> = ObserverRegistry()
) : Observable<org.mozilla.fenix.components.TabCollectionStorage.Observer> by delegate { ) : Observable<org.mozilla.fenix.components.TabCollectionStorage.Observer> by delegate {
@ -40,12 +37,12 @@ class TabCollectionStorage(
/** /**
* A collection has been created * A collection has been created
*/ */
fun onCollectionCreated(title: String, sessions: List<Session>, id: Long?) = Unit fun onCollectionCreated(title: String, sessions: List<TabSessionState>, id: Long?) = Unit
/** /**
* Tab(s) have been added to collection * Tab(s) have been added to collection
*/ */
fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) = Unit fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) = Unit
/** /**
* Collection has been renamed * Collection has been renamed
@ -58,32 +55,24 @@ class TabCollectionStorage(
private val collectionStorage by lazy { private val collectionStorage by lazy {
strictMode.resetAfter(StrictMode.allowThreadDiskReads()) { strictMode.resetAfter(StrictMode.allowThreadDiskReads()) {
TabCollectionStorage(context, sessionManager) TabCollectionStorage(context)
} }
} }
suspend fun createCollection(title: String, sessions: List<Session>) = ioScope.launch { suspend fun createCollection(title: String, sessions: List<TabSessionState>) = ioScope.launch {
val id = collectionStorage.createCollection(title, sessions) val id = collectionStorage.createCollection(title, sessions)
notifyObservers { onCollectionCreated(title, sessions, id) } notifyObservers { onCollectionCreated(title, sessions, id) }
}.join() }.join()
suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List<Session>) = ioScope.launch { suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List<TabSessionState>) = ioScope.launch {
collectionStorage.addTabsToCollection(tabCollection, sessions) collectionStorage.addTabsToCollection(tabCollection, sessions)
notifyObservers { onTabsAdded(tabCollection, sessions) } notifyObservers { onTabsAdded(tabCollection, sessions) }
}.join() }.join()
fun getTabCollectionsCount(): Int {
return collectionStorage.getTabCollectionsCount()
}
fun getCollections(): LiveData<List<TabCollection>> { fun getCollections(): LiveData<List<TabCollection>> {
return collectionStorage.getCollections().asLiveData() return collectionStorage.getCollections().asLiveData()
} }
fun getCollectionsPaged(): DataSource.Factory<Int, TabCollection> {
return collectionStorage.getCollectionsPaged()
}
suspend fun removeCollection(tabCollection: TabCollection) = ioScope.launch { suspend fun removeCollection(tabCollection: TabCollection) = ioScope.launch {
collectionStorage.removeCollection(tabCollection) collectionStorage.removeCollection(tabCollection)
}.join() }.join()

@ -7,8 +7,6 @@ package org.mozilla.fenix.components
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicyForSessionTypes import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicyForSessionTypes
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
/** /**
@ -49,7 +47,7 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
return TrackingProtectionPolicy.select( return TrackingProtectionPolicy.select(
cookiePolicy = getCustomCookiePolicy(), cookiePolicy = getCustomCookiePolicy(),
trackingCategories = getCustomTrackingCategories(), trackingCategories = getCustomTrackingCategories(),
cookiePurging = Config.channel.isNightlyOrDebug cookiePurging = getCustomCookiePurgingPolicy()
).let { ).let {
if (settings.blockTrackingContentSelectionInCustomTrackingProtection == "private") { if (settings.blockTrackingContentSelectionInCustomTrackingProtection == "private") {
it.forPrivateSessionsOnly() it.forPrivateSessionsOnly()
@ -95,6 +93,10 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
return categories.toTypedArray() return categories.toTypedArray()
} }
private fun getCustomCookiePurgingPolicy(): Boolean {
return settings.blockRedirectTrackersInCustomTrackingProtection
}
} }
@VisibleForTesting @VisibleForTesting
@ -103,6 +105,6 @@ internal fun TrackingProtectionPolicyForSessionTypes.adaptPolicyToChannel(): Tra
trackingCategories = trackingCategories, trackingCategories = trackingCategories,
cookiePolicy = cookiePolicy, cookiePolicy = cookiePolicy,
strictSocialTrackingProtection = strictSocialTrackingProtection, strictSocialTrackingProtection = strictSocialTrackingProtection,
cookiePurging = FeatureFlags.etpCookiePurging cookiePurging = cookiePurging
) )
} }

@ -18,6 +18,7 @@ import mozilla.components.feature.search.ext.toDefaultSearchEngineProvider
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.session.SettingsUseCases import mozilla.components.feature.session.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases import mozilla.components.feature.top.sites.TopSitesUseCases
@ -48,6 +49,13 @@ class UseCases(
*/ */
val tabsUseCases: TabsUseCases by lazyMonitored { TabsUseCases(store, sessionManager) } val tabsUseCases: TabsUseCases by lazyMonitored { TabsUseCases(store, sessionManager) }
/**
* Use cases for managing custom tabs.
*/
val customTabsUseCases: CustomTabsUseCases by lazyMonitored {
CustomTabsUseCases(sessionManager, sessionUseCases.loadUrl)
}
/** /**
* Use cases that provide search engine integration. * Use cases that provide search engine integration.
*/ */

@ -122,6 +122,7 @@ sealed class Event {
object NotificationMediaPlay : Event() object NotificationMediaPlay : Event()
object NotificationMediaPause : Event() object NotificationMediaPause : Event()
object TopSiteOpenDefault : Event() object TopSiteOpenDefault : Event()
object TopSiteOpenGoogle : Event()
object TopSiteOpenFrecent : Event() object TopSiteOpenFrecent : Event()
object TopSiteOpenPinned : Event() object TopSiteOpenPinned : Event()
object TopSiteOpenInNewTab : Event() object TopSiteOpenInNewTab : Event()
@ -168,6 +169,7 @@ sealed class Event {
object ContextualHintETPOutsideTap : Event() object ContextualHintETPOutsideTap : Event()
object ContextualHintETPInsideTap : Event() object ContextualHintETPInsideTap : Event()
// Tab tray
object TabsTrayOpened : Event() object TabsTrayOpened : Event()
object TabsTrayClosed : Event() object TabsTrayClosed : Event()
object OpenedExistingTab : Event() object OpenedExistingTab : Event()
@ -180,6 +182,8 @@ sealed class Event {
object TabsTraySaveToCollectionPressed : Event() object TabsTraySaveToCollectionPressed : Event()
object TabsTrayShareAllTabsPressed : Event() object TabsTrayShareAllTabsPressed : Event()
object TabsTrayCloseAllTabsPressed : Event() object TabsTrayCloseAllTabsPressed : Event()
object TabsTrayCfrDismissed : Event()
object TabsTrayCfrTapped : Event()
object ProgressiveWebAppOpenFromHomescreenTap : Event() object ProgressiveWebAppOpenFromHomescreenTap : Event()
object ProgressiveWebAppInstallAsShortcut : Event() object ProgressiveWebAppInstallAsShortcut : Event()
@ -195,6 +199,11 @@ sealed class Event {
object RecentlyClosedTabsOpened : Event() object RecentlyClosedTabsOpened : Event()
object ContextMenuCopyTapped : Event()
object ContextMenuSearchTapped : Event()
object ContextMenuSelectAllTapped : Event()
object ContextMenuShareTapped : Event()
// Interaction events with extras // Interaction events with extras
data class TopSiteSwipeCarousel(val page: Int) : Event() { data class TopSiteSwipeCarousel(val page: Int) : Event() {
@ -438,6 +447,7 @@ sealed class Event {
data class Action(override val engineSource: EngineSource) : EventSource(engineSource) data class Action(override val engineSource: EngineSource) : EventSource(engineSource)
data class Widget(override val engineSource: EngineSource) : EventSource(engineSource) data class Widget(override val engineSource: EngineSource) : EventSource(engineSource)
data class Shortcut(override val engineSource: EngineSource) : EventSource(engineSource) data class Shortcut(override val engineSource: EngineSource) : EventSource(engineSource)
data class TopSite(override val engineSource: EngineSource) : EventSource(engineSource)
data class Other(override val engineSource: EngineSource) : EventSource(engineSource) data class Other(override val engineSource: EngineSource) : EventSource(engineSource)
private val label: String private val label: String
@ -446,6 +456,7 @@ sealed class Event {
is Action -> "action" is Action -> "action"
is Widget -> "widget" is Widget -> "widget"
is Shortcut -> "shortcut" is Shortcut -> "shortcut"
is TopSite -> "topsite"
is Other -> "other" is Other -> "other"
} }
@ -457,7 +468,7 @@ sealed class Event {
} }
enum class SearchAccessPoint { enum class SearchAccessPoint {
SUGGESTION, ACTION, WIDGET, SHORTCUT, NONE SUGGESTION, ACTION, WIDGET, SHORTCUT, TOPSITE, NONE
} }
override val extras: Map<Events.performedSearchKeys, String>? override val extras: Map<Events.performedSearchKeys, String>?

@ -22,6 +22,7 @@ import org.mozilla.fenix.GleanMetrics.BrowserSearch
import org.mozilla.fenix.GleanMetrics.Collections import org.mozilla.fenix.GleanMetrics.Collections
import org.mozilla.fenix.GleanMetrics.ContextMenu import org.mozilla.fenix.GleanMetrics.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection
import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CrashReporter import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.CustomTab import org.mozilla.fenix.GleanMetrics.CustomTab
import org.mozilla.fenix.GleanMetrics.DownloadNotification import org.mozilla.fenix.GleanMetrics.DownloadNotification
@ -54,6 +55,7 @@ import org.mozilla.fenix.GleanMetrics.SyncAuth
import org.mozilla.fenix.GleanMetrics.Tab import org.mozilla.fenix.GleanMetrics.Tab
import org.mozilla.fenix.GleanMetrics.Tabs import org.mozilla.fenix.GleanMetrics.Tabs
import org.mozilla.fenix.GleanMetrics.TabsTray import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.TabsTrayCfr
import org.mozilla.fenix.GleanMetrics.Tip import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites import org.mozilla.fenix.GleanMetrics.TopSites
@ -513,6 +515,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TopSiteOpenDefault -> EventWrapper<NoExtraKeys>( is Event.TopSiteOpenDefault -> EventWrapper<NoExtraKeys>(
{ TopSites.openDefault.record(it) } { TopSites.openDefault.record(it) }
) )
is Event.TopSiteOpenGoogle -> EventWrapper<NoExtraKeys>(
{ TopSites.openGoogleSearchAttribution.record(it) }
)
is Event.TopSiteOpenFrecent -> EventWrapper<NoExtraKeys>( is Event.TopSiteOpenFrecent -> EventWrapper<NoExtraKeys>(
{ TopSites.openFrecency.record(it) } { TopSites.openFrecency.record(it) }
) )
@ -658,7 +663,13 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>( is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ TabsTray.closeAllTabs.record(it) } { TabsTray.closeAllTabs.record(it) }
) )
Event.AutoPlaySettingVisited -> EventWrapper<NoExtraKeys>( is Event.TabsTrayCfrDismissed -> EventWrapper<NoExtraKeys>(
{ TabsTrayCfr.dismiss.record(it) }
)
is Event.TabsTrayCfrTapped -> EventWrapper<NoExtraKeys>(
{ TabsTrayCfr.goToSettings.record(it) }
)
is Event.AutoPlaySettingVisited -> EventWrapper<NoExtraKeys>(
{ Autoplay.visitedSetting.record(it) } { Autoplay.visitedSetting.record(it) }
) )
is Event.AutoPlaySettingChanged -> EventWrapper( is Event.AutoPlaySettingChanged -> EventWrapper(
@ -691,15 +702,27 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.recentlyClosedTabsOpened.record(it) } { Events.recentlyClosedTabsOpened.record(it) }
) )
Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>( is Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>(
{ MasterPassword.displayed.record(it) } { MasterPassword.displayed.record(it) }
) )
Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>( is Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>(
{ MasterPassword.migration.record(it) } { MasterPassword.migration.record(it) }
) )
Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>( is Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>(
{ Tabs.settingOpened.record(it) } { Tabs.settingOpened.record(it) }
) )
Event.ContextMenuCopyTapped -> EventWrapper<NoExtraKeys>(
{ ContextualMenu.copyTapped.record(it) }
)
is Event.ContextMenuSearchTapped -> EventWrapper<NoExtraKeys>(
{ ContextualMenu.searchTapped.record(it) }
)
is Event.ContextMenuSelectAllTapped -> EventWrapper<NoExtraKeys>(
{ ContextualMenu.selectAllTapped.record(it) }
)
is Event.ContextMenuShareTapped -> EventWrapper<NoExtraKeys>(
{ ContextualMenu.shareTapped.record(it) }
)
// Don't record other events in Glean: // Don't record other events in Glean:
is Event.AddBookmark -> null is Event.AddBookmark -> null

@ -153,6 +153,15 @@ internal class ReleaseMetricController(
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> { Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) } metadata?.get("item")?.let { Event.ContextMenuItemTapped.create(it.toString()) }
} }
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.TEXT_SELECTION_OPTION -> {
when (metadata?.get("textSelectionOption")?.toString()) {
CONTEXT_MENU_COPY -> Event.ContextMenuCopyTapped
CONTEXT_MENU_SEARCH, CONTEXT_MENU_SEARCH_PRIVATELY -> Event.ContextMenuSearchTapped
CONTEXT_MENU_SELECT_ALL -> Event.ContextMenuSelectAllTapped
CONTEXT_MENU_SHARE -> Event.ContextMenuShareTapped
else -> null
}
}
Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> { Component.BROWSER_TOOLBAR to ToolbarFacts.Items.MENU -> {
metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened } metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened }
@ -235,4 +244,15 @@ internal class ReleaseMetricController(
} }
else -> null else -> null
} }
companion object {
/**
* Text selection long press context items to be tracked.
*/
const val CONTEXT_MENU_COPY = "org.mozilla.geckoview.COPY"
const val CONTEXT_MENU_SEARCH = "CUSTOM_CONTEXT_MENU_SEARCH"
const val CONTEXT_MENU_SEARCH_PRIVATELY = "CUSTOM_CONTEXT_MENU_SEARCH_PRIVATELY"
const val CONTEXT_MENU_SELECT_ALL = "org.mozilla.geckoview.SELECT_ALL"
const val CONTEXT_MENU_SHARE = "CUSTOM_CONTEXT_MENU_SHARE"
}
} }

@ -17,7 +17,6 @@ import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.support.base.log.logger.Logger import mozilla.components.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint
import org.mozilla.fenix.ext.components
import java.io.IOException import java.io.IOException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException import java.security.spec.InvalidKeySpecException
@ -58,6 +57,11 @@ object MetricsUtils {
engineSource engineSource
) )
) )
SearchAccessPoint.TOPSITE -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.TopSite(
engineSource
)
)
SearchAccessPoint.NONE -> Event.PerformedSearch( SearchAccessPoint.NONE -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Other( Event.PerformedSearch.EventSource.Other(
engineSource engineSource

@ -77,7 +77,7 @@ class DefaultBrowserToolbarController(
store.updateSearchTermsOfSelectedSession(text) store.updateSearchTermsOfSelectedSession(text)
activity.components.useCases.searchUseCases.defaultSearch.invoke( activity.components.useCases.searchUseCases.defaultSearch.invoke(
text, text,
sessionId = sessionManager.selectedSession?.id sessionId = store.state.selectedTabId
) )
} }

@ -24,6 +24,7 @@ import kotlinx.android.synthetic.main.component_browser_top_toolbar.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider import mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.ExternalAppType import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
@ -98,7 +99,6 @@ class BrowserToolbarView(
} }
with(container.context) { with(container.context) {
val sessionManager = components.core.sessionManager
val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported() val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported()
if (toolbarPosition == ToolbarPosition.TOP) { if (toolbarPosition == ToolbarPosition.TOP) {
@ -157,7 +157,10 @@ class BrowserToolbarView(
hint = secondaryTextColor, hint = secondaryTextColor,
separator = separatorColor, separator = separatorColor,
trackingProtection = primaryTextColor, trackingProtection = primaryTextColor,
permissionHighlights = primaryTextColor highlight = ContextCompat.getColor(
context,
R.color.whats_new_notification_color
)
) )
display.hint = context.getString(R.string.search_hint) display.hint = context.getString(R.string.search_hint)
@ -211,7 +214,7 @@ class BrowserToolbarView(
components.core.historyStorage, components.core.historyStorage,
lifecycleOwner, lifecycleOwner,
sessionId = null, sessionId = null,
isPrivate = sessionManager.selectedSession?.private ?: false, isPrivate = components.core.store.state.selectedTab?.content?.private ?: false,
interactor = interactor, interactor = interactor,
engine = components.core.engine engine = components.core.engine
) )

@ -24,7 +24,6 @@ import mozilla.components.feature.toolbar.ToolbarPresenter
import mozilla.components.lib.publicsuffixlist.PublicSuffixList import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.android.view.hideKeyboard import mozilla.components.support.ktx.android.view.hideKeyboard
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -120,20 +119,16 @@ class DefaultToolbarIntegration(
listOf( listOf(
DisplayToolbar.Indicators.TRACKING_PROTECTION, DisplayToolbar.Indicators.TRACKING_PROTECTION,
DisplayToolbar.Indicators.SECURITY, DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY DisplayToolbar.Indicators.EMPTY,
DisplayToolbar.Indicators.HIGHLIGHT
) )
} else { } else {
listOf( listOf(
DisplayToolbar.Indicators.SECURITY, DisplayToolbar.Indicators.SECURITY,
DisplayToolbar.Indicators.EMPTY DisplayToolbar.Indicators.EMPTY,
DisplayToolbar.Indicators.HIGHLIGHT
) )
} }
if (FeatureFlags.permissionIndicatorsToolbar) {
toolbar.display.indicators += DisplayToolbar.Indicators.PERMISSION_HIGHLIGHTS
}
toolbar.display.displayIndicatorSeparator =
context.settings().shouldUseTrackingProtection context.settings().shouldUseTrackingProtection
toolbar.display.icons = toolbar.display.icons.copy( toolbar.display.icons = toolbar.display.icons.copy(
@ -171,8 +166,7 @@ class DefaultToolbarIntegration(
interactor.onTabCounterClicked() interactor.onTabCounterClicked()
}, },
store = store, store = store,
menu = tabCounterMenu, menu = tabCounterMenu
privateColor = ContextCompat.getColor(context, R.color.primary_text_private_theme)
) )
val tabCount = if (isPrivate) { val tabCount = if (isPrivate) {

@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.lib.crash.Crash import mozilla.components.lib.crash.Crash
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.Components import org.mozilla.fenix.components.Components
@ -19,7 +18,7 @@ import org.mozilla.fenix.utils.Settings
class CrashReporterController( class CrashReporterController(
private val crash: Crash, private val crash: Crash,
private val session: Session?, private val sessionId: String?,
private val navController: NavController, private val navController: NavController,
private val components: Components, private val components: Components,
private val settings: Settings private val settings: Settings
@ -50,10 +49,10 @@ class CrashReporterController(
* @return Job if report is submitted through an IO thread, null otherwise * @return Job if report is submitted through an IO thread, null otherwise
*/ */
fun handleCloseAndRemove(sendCrash: Boolean): Job? { fun handleCloseAndRemove(sendCrash: Boolean): Job? {
session ?: return null sessionId ?: return null
val job = submitReportIfNecessary(sendCrash) val job = submitReportIfNecessary(sendCrash)
components.useCases.tabsUseCases.removeTab(session) components.useCases.tabsUseCases.removeTab(sessionId)
components.useCases.sessionUseCases.crashRecovery.invoke() components.useCases.sessionUseCases.crashRecovery.invoke()
navController.nav( navController.nav(

@ -32,7 +32,7 @@ class CrashReporterFragment : Fragment(R.layout.fragment_crash_reporter) {
val controller = CrashReporterController( val controller = CrashReporterController(
crash, crash,
session = requireComponents.core.sessionManager.selectedSession, sessionId = requireComponents.core.store.state.selectedTabId,
navController = findNavController(), navController = findNavController(),
components = requireComponents, components = requireComponents,
settings = requireContext().settings() settings = requireContext().settings()

@ -7,6 +7,7 @@ package org.mozilla.fenix.customtabs
import android.content.Context import android.content.Context
import android.graphics.Typeface import android.graphics.Typeface
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.getColor import androidx.core.content.ContextCompat.getColor
import mozilla.components.browser.menu.BrowserMenuBuilder import mozilla.components.browser.menu.BrowserMenuBuilder
import mozilla.components.browser.menu.BrowserMenuHighlight import mozilla.components.browser.menu.BrowserMenuHighlight
@ -17,8 +18,8 @@ import mozilla.components.browser.menu.item.BrowserMenuImageSwitch
import mozilla.components.browser.menu.item.BrowserMenuImageText import mozilla.components.browser.menu.item.BrowserMenuImageText
import mozilla.components.browser.menu.item.BrowserMenuItemToolbar import mozilla.components.browser.menu.item.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarMenu import org.mozilla.fenix.components.toolbar.ToolbarMenu
@ -46,7 +47,8 @@ class CustomTabToolbarMenu(
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) } override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
/** Gets the current custom tab session */ /** Gets the current custom tab session */
private val session: TabSessionState? get() = sessionId?.let { store.state.findTab(it) } @VisibleForTesting
internal val session: CustomTabSessionState? get() = sessionId?.let { store.state.findCustomTab(it) }
private val appName = context.getString(R.string.app_name) private val appName = context.getString(R.string.app_name)
override val menuToolbar by lazy { override val menuToolbar by lazy {

@ -11,6 +11,7 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.browser.toolbar.BrowserToolbar import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.display.DisplayToolbar import mozilla.components.browser.toolbar.display.DisplayToolbar
import mozilla.components.feature.customtabs.CustomTabsToolbarFeature import mozilla.components.feature.customtabs.CustomTabsToolbarFeature
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.support.base.feature.LifecycleAwareFeature import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.base.feature.UserInteractionHandler import mozilla.components.support.base.feature.UserInteractionHandler
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -20,6 +21,7 @@ import org.mozilla.fenix.ext.settings
class CustomTabsIntegration( class CustomTabsIntegration(
sessionManager: SessionManager, sessionManager: SessionManager,
store: BrowserStore, store: BrowserStore,
useCases: CustomTabsUseCases,
toolbar: BrowserToolbar, toolbar: BrowserToolbar,
sessionId: String, sessionId: String,
activity: Activity, activity: Activity,
@ -84,9 +86,10 @@ class CustomTabsIntegration(
} }
private val feature = CustomTabsToolbarFeature( private val feature = CustomTabsToolbarFeature(
sessionManager, store,
toolbar, toolbar,
sessionId, sessionId,
useCases,
menuBuilder = customTabToolbarMenu.menuBuilder, menuBuilder = customTabToolbarMenu.menuBuilder,
menuItemIndex = START_OF_MENU_ITEMS_INDEX, menuItemIndex = START_OF_MENU_ITEMS_INDEX,
window = activity.window, window = activity.window,

@ -9,7 +9,6 @@ import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination import androidx.navigation.NavDestination
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import kotlinx.android.synthetic.main.activity_home.* import kotlinx.android.synthetic.main.activity_home.*
import mozilla.components.browser.session.runWithSession
import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.WebAppManifestParser
@ -102,12 +101,10 @@ open class ExternalAppBrowserActivity : HomeActivity() {
// When this activity finishes, the process is staying around and the session still // When this activity finishes, the process is staying around and the session still
// exists then remove it now to free all its resources. Once this activity is finished // exists then remove it now to free all its resources. Once this activity is finished
// then there's no way to get back to it other than relaunching it. // then there's no way to get back to it other than relaunching it.
components.core.sessionManager.runWithSession(getExternalTabId()) { session -> val tabId = getExternalTabId()
// If the custom tag config has been removed we are opening this in normal browsing val customTab = tabId?.let { components.core.store.state.findCustomTab(it) }
if (session.customTabConfig != null) { if (tabId != null && customTab != null) {
remove(session) components.useCases.customTabsUseCases.remove(tabId)
}
true
} }
} }
} }

@ -14,7 +14,7 @@ import androidx.navigation.fragment.navArgs
import kotlinx.android.synthetic.main.component_browser_top_toolbar.* import kotlinx.android.synthetic.main.component_browser_top_toolbar.*
import kotlinx.android.synthetic.main.fragment_browser.* import kotlinx.android.synthetic.main.fragment_browser.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.manifest.WebAppManifestParser import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.concept.engine.manifest.getOrNull import mozilla.components.concept.engine.manifest.getOrNull
import mozilla.components.feature.contextmenu.ContextMenuCandidate import mozilla.components.feature.contextmenu.ContextMenuCandidate
@ -34,7 +34,6 @@ import org.mozilla.fenix.browser.CustomTabContextMenuCandidate
import org.mozilla.fenix.browser.FenixSnackbarDelegate import org.mozilla.fenix.browser.FenixSnackbarDelegate
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
@ -52,112 +51,109 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
private val hideToolbarFeature = ViewBoundFeatureWrapper<WebAppHideToolbarFeature>() private val hideToolbarFeature = ViewBoundFeatureWrapper<WebAppHideToolbarFeature>()
@Suppress("LongMethod", "ComplexMethod") @Suppress("LongMethod", "ComplexMethod")
override fun initializeUI(view: View): Session? { override fun initializeUI(view: View, tab: SessionState) {
return super.initializeUI(view)?.also { super.initializeUI(view, tab)
val activity = requireActivity()
val components = activity.components val customTabSessionId = customTabSessionId ?: return
val activity = requireActivity()
val manifest = args.webAppManifest?.let { json -> val components = activity.components
WebAppManifestParser().parse(json).getOrNull() val manifest = args.webAppManifest?.let { json -> WebAppManifestParser().parse(json).getOrNull() }
}
customTabsIntegration.set(
customTabSessionId?.let { customTabSessionId -> feature = CustomTabsIntegration(
customTabsIntegration.set( sessionManager = requireComponents.core.sessionManager,
feature = CustomTabsIntegration( store = requireComponents.core.store,
sessionManager = requireComponents.core.sessionManager, useCases = requireComponents.useCases.customTabsUseCases,
store = requireComponents.core.store, toolbar = toolbar,
toolbar = toolbar, sessionId = customTabSessionId,
sessionId = customTabSessionId, activity = activity,
activity = activity, onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) },
onItemTapped = { browserInteractor.onBrowserToolbarMenuItemTapped(it) }, isPrivate = tab.content.private,
isPrivate = it.private, shouldReverseItems = !activity.settings().shouldUseBottomToolbar
shouldReverseItems = !activity.settings().shouldUseBottomToolbar ),
), owner = this,
owner = this, view = view
view = view )
)
windowFeature.set( windowFeature.set(
feature = CustomTabWindowFeature( feature = CustomTabWindowFeature(
activity, activity,
components.core.store, components.core.store,
customTabSessionId customTabSessionId
) { uri -> ) { uri ->
val intent = val intent =
Intent.parseUri("${BuildConfig.DEEP_LINK_SCHEME}://open?url=$uri", 0) Intent.parseUri("${BuildConfig.DEEP_LINK_SCHEME}://open?url=$uri", 0)
if (intent.action == Intent.ACTION_VIEW) { if (intent.action == Intent.ACTION_VIEW) {
intent.addCategory(Intent.CATEGORY_BROWSABLE) intent.addCategory(Intent.CATEGORY_BROWSABLE)
intent.component = null intent.component = null
intent.selector = null intent.selector = null
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
} }
activity.startActivity(intent) activity.startActivity(intent)
}, },
owner = this, owner = this,
view = view view = view
) )
hideToolbarFeature.set( hideToolbarFeature.set(
feature = WebAppHideToolbarFeature( feature = WebAppHideToolbarFeature(
store = requireComponents.core.store, store = requireComponents.core.store,
customTabsStore = requireComponents.core.customTabsStore, customTabsStore = requireComponents.core.customTabsStore,
tabId = customTabSessionId, tabId = customTabSessionId,
manifest = manifest manifest = manifest
) { toolbarVisible -> ) { toolbarVisible ->
browserToolbarView.view.isVisible = toolbarVisible browserToolbarView.view.isVisible = toolbarVisible
webAppToolbarShouldBeVisible = toolbarVisible webAppToolbarShouldBeVisible = toolbarVisible
if (!toolbarVisible) { if (!toolbarVisible) {
engineView.setDynamicToolbarMaxHeight(0) engineView.setDynamicToolbarMaxHeight(0)
val browserEngine = val browserEngine =
swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams swipeRefresh.layoutParams as CoordinatorLayout.LayoutParams
browserEngine.bottomMargin = 0 browserEngine.bottomMargin = 0
} }
}, },
owner = this, owner = this,
view = toolbar view = toolbar
) )
if (manifest != null) { if (manifest != null) {
activity.lifecycle.addObservers( activity.lifecycle.addObservers(
WebAppActivityFeature( WebAppActivityFeature(
activity, activity,
components.core.icons, components.core.icons,
manifest manifest
), ),
ManifestUpdateFeature( ManifestUpdateFeature(
activity.applicationContext, activity.applicationContext,
requireComponents.core.store, requireComponents.core.store,
requireComponents.core.webAppShortcutManager, requireComponents.core.webAppShortcutManager,
requireComponents.core.webAppManifestStorage, requireComponents.core.webAppManifestStorage,
customTabSessionId, customTabSessionId,
manifest manifest
) )
) )
viewLifecycleOwner.lifecycle.addObserver( viewLifecycleOwner.lifecycle.addObserver(
WebAppSiteControlsFeature( WebAppSiteControlsFeature(
activity.applicationContext, activity.applicationContext,
requireComponents.core.store, requireComponents.core.store,
requireComponents.useCases.sessionUseCases.reload, requireComponents.useCases.sessionUseCases.reload,
customTabSessionId, customTabSessionId,
manifest, manifest,
WebAppSiteControlsBuilder( WebAppSiteControlsBuilder(
requireComponents.core.sessionManager, requireComponents.core.sessionManager,
requireComponents.useCases.sessionUseCases.reload, requireComponents.useCases.sessionUseCases.reload,
customTabSessionId, customTabSessionId,
manifest manifest
)
)
)
} else {
viewLifecycleOwner.lifecycle.addObserver(
PoweredByNotification(
activity.applicationContext,
requireComponents.core.store,
customTabSessionId
)
) )
} )
} )
} else {
viewLifecycleOwner.lifecycle.addObserver(
PoweredByNotification(
activity.applicationContext,
requireComponents.core.store,
customTabSessionId
)
)
} }
} }
@ -181,28 +177,29 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded() return customTabsIntegration.onBackPressed() || super.removeSessionIfNeeded()
} }
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) { override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val directions = ExternalAppBrowserFragmentDirections val directions = ExternalAppBrowserFragmentDirections
.actionGlobalQuickSettingsSheetDialogFragment( .actionGlobalQuickSettingsSheetDialogFragment(
sessionId = session.id, sessionId = tab.id,
url = session.url, url = tab.content.url,
title = session.title, title = tab.content.title,
isSecured = session.securityInfo.secure, isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions, sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(), gravity = getAppropriateLayoutGravity(),
certificateName = session.securityInfo.issuer certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights
) )
nav(R.id.externalAppBrowserFragment, directions) nav(R.id.externalAppBrowserFragment, directions)
} }
override fun navToTrackingProtectionPanel(session: Session) { override fun navToTrackingProtectionPanel(tab: SessionState) {
requireComponents.useCases.trackingProtectionUseCases.containsException(session.id) { contains -> requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
val isEnabled = session.trackerBlockingEnabled && !contains val isEnabled = tab.trackingProtection.enabled && !contains
val directions = val directions =
ExternalAppBrowserFragmentDirections ExternalAppBrowserFragmentDirections
.actionGlobalTrackingProtectionPanelDialogFragment( .actionGlobalTrackingProtectionPanelDialogFragment(
sessionId = session.id, sessionId = tab.id,
url = session.url, url = tab.content.url,
trackingProtectionEnabled = isEnabled, trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity() gravity = getAppropriateLayoutGravity()
) )

@ -106,7 +106,7 @@ class FennecWebAppIntentProcessor(
if (path.isNullOrEmpty()) return null if (path.isNullOrEmpty()) return null
val file = File(path) val file = File(path)
if (!file.isUnderFennecManifestDirectory()) return null if (!isUnderFennecManifestDirectory(file)) return null
return try { return try {
// Gecko in Fennec added some add some additional data, such as cached_icon, in // Gecko in Fennec added some add some additional data, such as cached_icon, in
@ -127,12 +127,13 @@ class FennecWebAppIntentProcessor(
/** /**
* Fennec manifests should be located in <filesDir>/mozilla/<profile>/manifests/ * Fennec manifests should be located in <filesDir>/mozilla/<profile>/manifests/
*/ */
private fun File.isUnderFennecManifestDirectory(): Boolean { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
val manifestsDir = canonicalFile.parentFile internal fun isUnderFennecManifestDirectory(file: File): Boolean {
val manifestsDir = file.canonicalFile.parentFile
// Check that manifest is in a folder named "manifests" // Check that manifest is in a folder named "manifests"
return manifestsDir == null || manifestsDir.name != "manifests" || return manifestsDir != null && manifestsDir.name == "manifests" &&
// Check that the folder two levels up is named "mozilla" // Check that the folder two levels up is named "mozilla"
manifestsDir.parentFile?.parentFile != getMozillaDirectory() manifestsDir.parentFile?.parentFile?.canonicalPath == getMozillaDirectory().canonicalPath
} }
private fun createFallbackCustomTabConfig(): CustomTabConfig { private fun createFallbackCustomTabConfig(): CustomTabConfig {

@ -58,7 +58,7 @@ class PoweredByNotification(
val appName = getString(R.string.app_name) val appName = getString(R.string.app_name)
return NotificationCompat.Builder(this, channelId) return NotificationCompat.Builder(this, channelId)
.setSmallIcon(R.drawable.ic_status_logo) .setSmallIcon(R.drawable.ic_status_logo)
.setContentTitle(getString(R.string.browser_menu_powered_by2, appName)) .setContentTitle(applicationContext.getString(R.string.browser_menu_powered_by2, appName))
.setBadgeIconType(BADGE_ICON_NONE) .setBadgeIconType(BADGE_ICON_NONE)
.setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme)) .setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme))
.setPriority(NotificationCompat.PRIORITY_MIN) .setPriority(NotificationCompat.PRIORITY_MIN)

@ -6,9 +6,11 @@ package org.mozilla.fenix.downloads
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.AbstractFetchDownloadService import mozilla.components.feature.downloads.AbstractFetchDownloadService
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
class DownloadService : AbstractFetchDownloadService() { class DownloadService : AbstractFetchDownloadService() {
override val httpClient by lazy { components.core.client } override val httpClient by lazy { components.core.client }
override val store: BrowserStore by lazy { components.core.store } override val store: BrowserStore by lazy { components.core.store }
override val style: Style by lazy { Style(R.color.notification_accent_color_normal_theme) }
} }

@ -38,6 +38,17 @@ fun Fragment.showToolbar(title: String) {
(activity as NavHostActivity).getSupportActionBarAndInflateIfNecessary().show() (activity as NavHostActivity).getSupportActionBarAndInflateIfNecessary().show()
} }
/**
* Run the [block] only if the [Fragment] is attached.
*
* @param block A callback to be executed if the container [Fragment] is attached.
*/
internal inline fun Fragment.runIfFragmentIsAttached(block: () -> Unit) {
context?.let {
block()
}
}
/** /**
* Hides the activity toolbar. * Hides the activity toolbar.
* Throws if the fragment is not attached to an [AppCompatActivity]. * Throws if the fragment is not attached to an [AppCompatActivity].

@ -21,6 +21,7 @@ import android.view.accessibility.AccessibilityEvent
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.PopupWindow import android.widget.PopupWindow
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
@ -63,13 +64,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot import mozilla.appservices.places.BookmarkRoot
import mozilla.components.browser.menu.view.MenuButton import mozilla.components.browser.menu.view.MenuButton
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.selector.normalTabs import mozilla.components.browser.state.selector.normalTabs
import mozilla.components.browser.state.selector.privateTabs import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.sync.AccountObserver import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType import mozilla.components.concept.sync.AuthType
import mozilla.components.concept.sync.OAuthAccount import mozilla.components.concept.sync.OAuthAccount
@ -103,6 +104,7 @@ import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController import org.mozilla.fenix.home.sessioncontrol.DefaultSessionControlController
import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor import org.mozilla.fenix.home.sessioncontrol.SessionControlInteractor
import org.mozilla.fenix.home.sessioncontrol.SessionControlView import org.mozilla.fenix.home.sessioncontrol.SessionControlView
@ -144,8 +146,6 @@ class HomeFragment : Fragment() {
} }
} }
private val sessionManager: SessionManager
get() = requireComponents.core.sessionManager
private val store: BrowserStore private val store: BrowserStore
get() = requireComponents.core.store get() = requireComponents.core.store
@ -234,10 +234,11 @@ class HomeFragment : Fragment() {
engine = components.core.engine, engine = components.core.engine,
metrics = components.analytics.metrics, metrics = components.analytics.metrics,
store = store, store = store,
sessionManager = sessionManager,
tabCollectionStorage = components.core.tabCollectionStorage, tabCollectionStorage = components.core.tabCollectionStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab, addTabUseCase = components.useCases.tabsUseCases.addTab,
restoreUseCase = components.useCases.tabsUseCases.restore,
reloadUrlUseCase = components.useCases.sessionUseCases.reload, reloadUrlUseCase = components.useCases.sessionUseCases.reload,
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
fragmentStore = homeFragmentStore, fragmentStore = homeFragmentStore,
navController = findNavController(), navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope, viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
@ -273,9 +274,13 @@ class HomeFragment : Fragment() {
* Returns a [TopSitesConfig] which specifies how many top sites to display and whether or * Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
* not frequently visited sites should be displayed. * not frequently visited sites should be displayed.
*/ */
private fun getTopSitesConfig(): TopSitesConfig { @VisibleForTesting
internal fun getTopSitesConfig(): TopSitesConfig {
val settings = requireContext().settings() val settings = requireContext().settings()
return TopSitesConfig(settings.topSitesMaxLimit, settings.showTopFrecentSites) return TopSitesConfig(
settings.topSitesMaxLimit,
if (settings.showTopFrecentSites) FrecencyThresholdOption.SKIP_ONE_TIME_PAGES else null
)
} }
/** /**
@ -425,7 +430,8 @@ class HomeFragment : Fragment() {
if (searchEngine != null) { if (searchEngine != null) {
val iconSize = val iconSize =
requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size) requireContext().resources.getDimensionPixelSize(R.dimen.preference_icon_drawable_size)
val searchIcon = BitmapDrawable(requireContext().resources, searchEngine.icon) val searchIcon =
BitmapDrawable(requireContext().resources, searchEngine.icon)
searchIcon.setBounds(0, 0, iconSize, iconSize) searchIcon.setBounds(0, 0, iconSize, iconSize)
search_engine_icon?.setImageDrawable(searchIcon) search_engine_icon?.setImageDrawable(searchIcon)
} else { } else {
@ -471,9 +477,9 @@ class HomeFragment : Fragment() {
private fun removeAllTabsAndShowSnackbar(sessionCode: String) { private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
if (sessionCode == ALL_PRIVATE_TABS) { if (sessionCode == ALL_PRIVATE_TABS) {
sessionManager.removePrivateSessions() requireComponents.useCases.tabsUseCases.removePrivateTabs()
} else { } else {
sessionManager.removeNormalSessions() requireComponents.useCases.tabsUseCases.removeNormalTabs()
} }
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) { val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
@ -587,7 +593,9 @@ class HomeFragment : Fragment() {
} }
if (browsingModeManager.mode.isPrivate && if (browsingModeManager.mode.isPrivate &&
context.settings().showPrivateModeCfr // We will be showing the search dialog and don't want to show the CFR while the dialog shows
!bundleArgs.getBoolean(FOCUS_ON_ADDRESS_BAR) &&
context.settings().shouldShowPrivateModeCfr
) { ) {
recommendPrivateBrowsingShortcut() recommendPrivateBrowsingShortcut()
} }
@ -693,10 +701,13 @@ class HomeFragment : Fragment() {
// We want to show the popup only after privateBrowsingButton is available. // We want to show the popup only after privateBrowsingButton is available.
// Otherwise, we will encounter an activity token error. // Otherwise, we will encounter an activity token error.
privateBrowsingButton.post { privateBrowsingButton.post {
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis() runIfFragmentIsAttached {
privateBrowsingRecommend.showAsDropDown( context.settings().showedPrivateModeContextualFeatureRecommender = true
privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
) privateBrowsingRecommend.showAsDropDown(
privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
)
}
} }
} }
} }
@ -1002,7 +1013,6 @@ class HomeFragment : Fragment() {
// https://github.com/mozilla-mobile/fenix/issues/16792 // https://github.com/mozilla-mobile/fenix/issues/16792
private fun updateTabCounter(browserState: BrowserState) { private fun updateTabCounter(browserState: BrowserState) {
val tabCount = if (browsingModeManager.mode.isPrivate) { val tabCount = if (browsingModeManager.mode.isPrivate) {
view?.tab_button?.setColor(ContextCompat.getColor(requireContext(), R.color.primary_text_private_theme))
browserState.privateTabs.size browserState.privateTabs.size
} else { } else {
browserState.normalTabs.size browserState.normalTabs.size
@ -1022,7 +1032,6 @@ class HomeFragment : Fragment() {
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar" private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
private const val FOCUS_ON_COLLECTION = "focusOnCollection" private const val FOCUS_ON_COLLECTION = "focusOnCollection"
private const val ANIMATION_DELAY = 100L
/** /**
* Represents the number of items in [sessionControlView] that are NOT part of * Represents the number of items in [sessionControlView] that are NOT part of

@ -1,30 +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.Intent
import androidx.navigation.NavController
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.sessionsOfType
/**
* The Private Browsing Mode notification has an "Delete and Open" button to let users delete all
* of their private tabs.
*/
class NotificationsIntentProcessor(
private val activity: HomeActivity
) : HomeIntentProcessor {
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
return if (intent.extras?.getBoolean(HomeActivity.EXTRA_DELETE_PRIVATE_TABS) == true) {
out.putExtra(HomeActivity.EXTRA_DELETE_PRIVATE_TABS, false)
activity.components.core.sessionManager.run {
sessionsOfType(private = true).forEach { remove(it) }
}
true
} else intent.extras?.getBoolean(HomeActivity.EXTRA_OPENED_FROM_NOTIFICATION) == true
}
}

@ -6,6 +6,7 @@ package org.mozilla.fenix.home.intent
import android.content.Intent import android.content.Intent
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.state.selector.findTab
import mozilla.components.feature.media.service.AbstractMediaService import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
@ -25,11 +26,14 @@ class OpenSpecificTabIntentProcessor(
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean { override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
if (intent.action == getAction()) { if (intent.action == getAction()) {
val sessionManager = activity.components.core.sessionManager val browserStore = activity.components.core.store
val sessionId = intent.extras?.getString(getTabId()) val tabId = intent.extras?.getString(getTabId())
val session = sessionId?.let { sessionManager.findSessionById(it) }
// Technically the additional lookup here isn't necessary, but this way we
// can make sure that we never try and select a custom tab by mistake.
val session = tabId?.let { browserStore.state.findTab(tabId) }
if (session != null) { if (session != null) {
sessionManager.select(session) activity.components.useCases.tabsUseCases.selectTab(tabId)
activity.openToBrowser(BrowserDirection.FromGlobal) activity.openToBrowser(BrowserDirection.FromGlobal)
return true return true
} }

@ -43,7 +43,12 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
ButtonTipViewHolder.LAYOUT_ID ButtonTipViewHolder.LAYOUT_ID
) )
data class TopSitePager(val topSites: List<TopSite>) : AdapterItem(TopSitePagerViewHolder.LAYOUT_ID) { data class TopSitePagerPayload(
val changed: Set<Pair<Int, TopSite>>
)
data class TopSitePager(val topSites: List<TopSite>) :
AdapterItem(TopSitePagerViewHolder.LAYOUT_ID) {
override fun sameAs(other: AdapterItem): Boolean { override fun sameAs(other: AdapterItem): Boolean {
val newTopSites = (other as? TopSitePager) ?: return false val newTopSites = (other as? TopSitePager) ?: return false
return newTopSites.topSites.size == this.topSites.size return newTopSites.topSites.size == this.topSites.size
@ -56,6 +61,19 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
val oldTopSites = this.topSites.asSequence() val oldTopSites = this.topSites.asSequence()
return newSitesSequence.zip(oldTopSites).all { (new, old) -> new == old } return newSitesSequence.zip(oldTopSites).all { (new, old) -> new == old }
} }
override fun getChangePayload(newItem: AdapterItem): Any? {
val newTopSites = (newItem as? TopSitePager) ?: return null
val oldTopSites = (this as? TopSitePager) ?: return null
val changed = mutableSetOf<Pair<Int, TopSite>>()
for ((index, item) in newTopSites.topSites.withIndex()) {
if (oldTopSites.topSites.getOrNull(index) != item) {
changed.add(Pair(index, item))
}
}
return if (changed.isNotEmpty()) TopSitePagerPayload(changed) else null
}
} }
object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID) object PrivateBrowsingDescription : AdapterItem(PrivateBrowsingDescriptionViewHolder.LAYOUT_ID)
@ -195,6 +213,25 @@ class SessionControlAdapter(
override fun getItemViewType(position: Int) = getItem(position).viewType override fun getItemViewType(position: Int) = getItem(position).viewType
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNullOrEmpty()) {
onBindViewHolder(holder, position)
} else {
when (holder) {
is TopSitePagerViewHolder -> {
if (payloads[0] is AdapterItem.TopSitePagerPayload) {
val payload = payloads[0] as AdapterItem.TopSitePagerPayload
holder.update(payload)
}
}
}
}
}
@SuppressWarnings("ComplexMethod") @SuppressWarnings("ComplexMethod")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position) val item = getItem(position)

@ -6,19 +6,21 @@ package org.mozilla.fenix.home.sessioncontrol
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.EditText import android.widget.EditText
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.session.SessionManager import mozilla.components.browser.state.selector.getNormalOrPrivateTabs
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionUseCases import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tab.collections.TabCollection import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tab.collections.ext.restore import mozilla.components.feature.tab.collections.ext.invoke
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.android.view.showKeyboard
@ -36,7 +38,6 @@ import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.metrics import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections import org.mozilla.fenix.home.HomeFragmentDirections
@ -173,11 +174,12 @@ class DefaultSessionControlController(
private val settings: Settings, private val settings: Settings,
private val engine: Engine, private val engine: Engine,
private val metrics: MetricController, private val metrics: MetricController,
private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
private val tabCollectionStorage: TabCollectionStorage, private val tabCollectionStorage: TabCollectionStorage,
private val addTabUseCase: TabsUseCases.AddNewTabUseCase, private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
private val restoreUseCase: TabsUseCases.RestoreUseCase,
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase, private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val fragmentStore: HomeFragmentStore, private val fragmentStore: HomeFragmentStore,
private val navController: NavController, private val navController: NavController,
private val viewLifecycleScope: CoroutineScope, private val viewLifecycleScope: CoroutineScope,
@ -208,13 +210,15 @@ class DefaultSessionControlController(
override fun handleCollectionOpenTabClicked(tab: ComponentTab) { override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
dismissSearchDialogIfDisplayed() dismissSearchDialogIfDisplayed()
sessionManager.restore(
restoreUseCase.invoke(
activity, activity,
engine, engine,
tab, tab,
onTabRestored = { onTabRestored = {
activity.openToBrowser(BrowserDirection.FromHome) activity.openToBrowser(BrowserDirection.FromHome)
reloadUrlUseCase.invoke(sessionManager.selectedSession) selectTabUseCase.invoke(it)
reloadUrlUseCase.invoke(it)
}, },
onFailure = { onFailure = {
activity.openToBrowserAndLoad( activity.openToBrowserAndLoad(
@ -229,7 +233,7 @@ class DefaultSessionControlController(
} }
override fun handleCollectionOpenTabsTapped(collection: TabCollection) { override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
sessionManager.restore( restoreUseCase.invoke(
activity, activity,
engine, engine,
collection, collection,
@ -319,12 +323,9 @@ class DefaultSessionControlController(
setTitle(R.string.rename_top_site) setTitle(R.string.rename_top_site)
setView(customLayout) setView(customLayout)
setPositiveButton(R.string.top_sites_rename_dialog_ok) { dialog, _ -> setPositiveButton(R.string.top_sites_rename_dialog_ok) { dialog, _ ->
val newTitle = topSiteLabelEditText.text.toString() viewLifecycleScope.launch(Dispatchers.IO) {
if (newTitle.isNotBlank()) { with(activity.components.useCases.topSitesUseCase) {
viewLifecycleScope.launch(Dispatchers.IO) { renameTopSites(topSite, topSiteLabelEditText.text.toString())
with(activity.components.useCases.topSitesUseCase) {
renameTopSites(topSite, newTitle)
}
} }
} }
dialog.dismiss() dialog.dismiss()
@ -362,24 +363,71 @@ class DefaultSessionControlController(
override fun handleSelectTopSite(url: String, type: TopSite.Type) { override fun handleSelectTopSite(url: String, type: TopSite.Type) {
dismissSearchDialogIfDisplayed() dismissSearchDialogIfDisplayed()
metrics.track(Event.TopSiteOpenInNewTab) metrics.track(Event.TopSiteOpenInNewTab)
when (type) { when (type) {
TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault) TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent) TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned) TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
} }
if (url == SupportUtils.GOOGLE_URL) {
metrics.track(Event.TopSiteOpenGoogle)
}
if (url == SupportUtils.POCKET_TRENDING_URL) { if (url == SupportUtils.POCKET_TRENDING_URL) {
metrics.track(Event.PocketTopSiteClicked) metrics.track(Event.PocketTopSiteClicked)
} }
if (SupportUtils.GOOGLE_URL.equals(url, true)) {
val availableEngines = getAvailableSearchEngines()
val searchAccessPoint = Event.PerformedSearch.SearchAccessPoint.TOPSITE
val event =
availableEngines.firstOrNull { engine -> engine.suggestUrl?.contains(url) == true }
?.let { searchEngine ->
searchAccessPoint.let { sap ->
MetricsUtils.createSearchEvent(searchEngine, store, sap)
}
}
event?.let { activity.metrics.track(it) }
}
addTabUseCase.invoke( addTabUseCase.invoke(
url = url, url = appendSearchAttributionToUrlIfNeeded(url),
selectTab = true, selectTab = true,
startLoading = true startLoading = true
) )
activity.openToBrowser(BrowserDirection.FromHome) activity.openToBrowser(BrowserDirection.FromHome)
} }
@VisibleForTesting
internal fun getAvailableSearchEngines() = activity
.components
.core
.store
.state
.search
.searchEngines
/**
* Append a search attribution query to any provided search engine URL based on the
* user's current region.
*/
private fun appendSearchAttributionToUrlIfNeeded(url: String): String {
if (url == SupportUtils.GOOGLE_URL) {
store.state.search.region?.let { region ->
return when (region.current) {
"US" -> SupportUtils.GOOGLE_US_URL
else -> SupportUtils.GOOGLE_XX_URL
}
}
}
return url
}
private fun dismissSearchDialogIfDisplayed() { private fun dismissSearchDialogIfDisplayed() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) { if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp() navController.navigateUp()
@ -436,8 +484,8 @@ class DefaultSessionControlController(
// Only register the observer right before moving to collection creation // Only register the observer right before moving to collection creation
registerCollectionStorageObserver() registerCollectionStorageObserver()
val tabIds = sessionManager val tabIds = store.state
.sessionsOfType(private = activity.browsingModeManager.mode.isPrivate) .getNormalOrPrivateTabs(private = activity.browsingModeManager.mode.isPrivate)
.map { session -> session.id } .map { session -> session.id }
.toList() .toList()
.toTypedArray() .toTypedArray()

@ -13,6 +13,7 @@ import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.home.sessioncontrol.AdapterItem
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesPagerAdapter import org.mozilla.fenix.home.sessioncontrol.viewholders.topsites.TopSitesPagerAdapter
@ -28,7 +29,11 @@ class TopSitePagerViewHolder(
private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() { private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
if (currentPage != position) { if (currentPage != position) {
pageIndicator.context.components.analytics.metrics.track(Event.TopSiteSwipeCarousel(position)) pageIndicator.context.components.analytics.metrics.track(
Event.TopSiteSwipeCarousel(
position
)
)
} }
pageIndicator.setSelection(position) pageIndicator.setSelection(position)
@ -43,6 +48,12 @@ class TopSitePagerViewHolder(
} }
} }
fun update(payload: AdapterItem.TopSitePagerPayload) {
for (item in payload.changed) {
topSitesPagerAdapter.notifyItemChanged(currentPage, payload)
}
}
fun bind(topSites: List<TopSite>) { fun bind(topSites: List<TopSite>) {
val chunkedTopSites = topSites.chunked(TOP_SITES_PER_PAGE) val chunkedTopSites = topSites.chunked(TOP_SITES_PER_PAGE)
topSitesPagerAdapter.submitList(chunkedTopSites) topSitesPagerAdapter.submitList(chunkedTopSites)

@ -21,7 +21,7 @@ class OnboardingPrivacyNoticeViewHolder(
view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_privacy_notice) view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_privacy_notice)
val appName = view.context.getString(R.string.app_name) val appName = view.context.getString(R.string.app_name)
view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description, appName) view.description_text.text = view.context.getString(R.string.onboarding_privacy_notice_description2, appName)
view.read_button.setOnClickListener { view.read_button.setOnClickListener {
it.context.components.analytics.metrics.track(Event.OnboardingPrivacyNotice) it.context.components.analytics.metrics.track(Event.OnboardingPrivacyNotice)

@ -8,13 +8,14 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import kotlinx.android.synthetic.main.top_site_item.view.*
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.perf.StartupTimeline
class TopSitesAdapter( class TopSitesAdapter(
private val interactor: TopSiteInteractor private val interactor: TopSiteInteractor
) : ListAdapter<TopSite, TopSiteItemViewHolder>(DiffCallback) { ) : ListAdapter<TopSite, TopSiteItemViewHolder>(TopSitesDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteItemViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteItemViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
.inflate(TopSiteItemViewHolder.LAYOUT_ID, parent, false) .inflate(TopSiteItemViewHolder.LAYOUT_ID, parent, false)
@ -26,11 +27,39 @@ class TopSitesAdapter(
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
private object DiffCallback : DiffUtil.ItemCallback<TopSite>() { override fun onBindViewHolder(
override fun areItemsTheSame(oldItem: TopSite, newItem: TopSite) = holder: TopSiteItemViewHolder,
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNullOrEmpty()) {
onBindViewHolder(holder, position)
} else {
when (payloads[0]) {
is TopSite -> {
holder.bind((payloads[0] as TopSite))
}
is TopSitePayload -> {
holder.itemView.top_site_title.text = (payloads[0] as TopSitePayload).newTitle
}
}
}
}
data class TopSitePayload(
val newTitle: String?
)
internal object TopSitesDiffCallback : DiffUtil.ItemCallback<TopSite>() {
override fun areItemsTheSame(oldItem: TopSite, newItem: TopSite) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: TopSite, newItem: TopSite) = override fun areContentsTheSame(oldItem: TopSite, newItem: TopSite) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.url == newItem.url
override fun getChangePayload(oldItem: TopSite, newItem: TopSite): Any? {
return if (oldItem.id == newItem.id && oldItem.url == newItem.url && oldItem.title != newItem.title) {
TopSitePayload(newItem.title)
} else null
}
} }
} }

@ -10,12 +10,13 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import kotlinx.android.synthetic.main.component_top_sites.view.* import kotlinx.android.synthetic.main.component_top_sites.view.*
import mozilla.components.feature.top.sites.TopSite import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.sessioncontrol.AdapterItem
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder import org.mozilla.fenix.home.sessioncontrol.viewholders.TopSiteViewHolder
class TopSitesPagerAdapter( class TopSitesPagerAdapter(
private val interactor: TopSiteInteractor private val interactor: TopSiteInteractor
) : ListAdapter<List<TopSite>, TopSiteViewHolder>(DiffCallback) { ) : ListAdapter<List<TopSite>, TopSiteViewHolder>(TopSiteListDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder {
val view = LayoutInflater.from(parent.context) val view = LayoutInflater.from(parent.context)
@ -23,12 +24,30 @@ class TopSitesPagerAdapter(
return TopSiteViewHolder(view, interactor) return TopSiteViewHolder(view, interactor)
} }
override fun onBindViewHolder(
holder: TopSiteViewHolder,
position: Int,
payloads: MutableList<Any>
) {
if (payloads.isNullOrEmpty()) {
onBindViewHolder(holder, position)
} else {
if (payloads[0] is AdapterItem.TopSitePagerPayload) {
val adapter = holder.itemView.top_sites_list.adapter as TopSitesAdapter
val payload = payloads[0] as AdapterItem.TopSitePagerPayload
for (item in payload.changed) {
adapter.notifyItemChanged(item.first, item.second)
}
}
}
}
override fun onBindViewHolder(holder: TopSiteViewHolder, position: Int) { override fun onBindViewHolder(holder: TopSiteViewHolder, position: Int) {
val adapter = holder.itemView.top_sites_list.adapter as TopSitesAdapter val adapter = holder.itemView.top_sites_list.adapter as TopSitesAdapter
adapter.submitList(getItem(position)) adapter.submitList(getItem(position))
} }
private object DiffCallback : DiffUtil.ItemCallback<List<TopSite>>() { internal object TopSiteListDiffCallback : DiffUtil.ItemCallback<List<TopSite>>() {
override fun areItemsTheSame(oldItem: List<TopSite>, newItem: List<TopSite>): Boolean { override fun areItemsTheSame(oldItem: List<TopSite>, newItem: List<TopSite>): Boolean {
return oldItem.size == newItem.size return oldItem.size == newItem.size
} }
@ -36,5 +55,15 @@ class TopSitesPagerAdapter(
override fun areContentsTheSame(oldItem: List<TopSite>, newItem: List<TopSite>): Boolean { override fun areContentsTheSame(oldItem: List<TopSite>, newItem: List<TopSite>): Boolean {
return newItem.zip(oldItem).all { (new, old) -> new == old } return newItem.zip(oldItem).all { (new, old) -> new == old }
} }
override fun getChangePayload(oldItem: List<TopSite>, newItem: List<TopSite>): Any? {
val changed = mutableSetOf<Pair<Int, TopSite>>()
for ((index, item) in newItem.withIndex()) {
if (oldItem.getOrNull(index) != item) {
changed.add(Pair(index, item))
}
}
return if (changed.isNotEmpty()) AdapterItem.TopSitePagerPayload(changed) else null
}
} }
} }

@ -26,7 +26,9 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason import mozilla.components.service.fxa.sync.SyncReason
@ -94,7 +96,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
::deleteHistoryItems, ::deleteHistoryItems,
::syncHistory, ::syncHistory,
requireComponents.analytics.metrics requireComponents.analytics.metrics
) )
historyInteractor = HistoryInteractor( historyInteractor = HistoryInteractor(
historyController historyController
) )
@ -271,6 +273,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved) requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
requireComponents.core.historyStorage.deleteEverything() requireComponents.core.historyStorage.deleteEverything()
deleteOpenTabsEngineHistory(requireComponents.core.store)
launch(Main) { launch(Main) {
viewModel.invalidate() viewModel.invalidate()
historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode) historyStore.dispatch(HistoryFragmentAction.ExitDeletionMode)
@ -288,6 +291,10 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
} }
} }
private suspend fun deleteOpenTabsEngineHistory(store: BrowserStore) {
store.dispatch(EngineAction.PurgeHistoryAction).join()
}
private fun share(data: List<ShareData>) { private fun share(data: List<ShareData>) {
requireComponents.analytics.metrics.track(Event.HistoryItemShared) requireComponents.analytics.metrics.track(Event.HistoryItemShared)
val directions = HistoryFragmentDirections.actionGlobalShareFragment( val directions = HistoryFragmentDirections.actionGlobalShareFragment(

@ -8,11 +8,11 @@ import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
class RecentlyClosedAdapter( class RecentlyClosedAdapter(
private val interactor: RecentlyClosedFragmentInteractor private val interactor: RecentlyClosedFragmentInteractor
) : ListAdapter<ClosedTab, RecentlyClosedItemViewHolder>(DiffCallback) { ) : ListAdapter<RecoverableTab, RecentlyClosedItemViewHolder>(DiffCallback) {
override fun onCreateViewHolder( override fun onCreateViewHolder(
parent: ViewGroup, parent: ViewGroup,
viewType: Int viewType: Int
@ -26,11 +26,11 @@ class RecentlyClosedAdapter(
holder.bind(getItem(position)) holder.bind(getItem(position))
} }
private object DiffCallback : DiffUtil.ItemCallback<ClosedTab>() { private object DiffCallback : DiffUtil.ItemCallback<RecoverableTab>() {
override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) = override fun areItemsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) =
oldItem.id == newItem.id oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: ClosedTab, newItem: ClosedTab) = override fun areContentsTheSame(oldItem: RecoverableTab, newItem: RecoverableTab) =
oldItem.id == newItem.id oldItem.id == newItem.id
} }
} }

@ -9,12 +9,11 @@ import android.content.ClipboardManager
import android.content.res.Resources import android.content.res.Resources
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavOptions import androidx.navigation.NavOptions
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.recentlyclosed.ext.restoreTab import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
@ -22,29 +21,29 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar import org.mozilla.fenix.components.FenixSnackbar
interface RecentlyClosedController { interface RecentlyClosedController {
fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null) fun handleOpen(item: RecoverableTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: ClosedTab) fun handleDeleteOne(tab: RecoverableTab)
fun handleCopyUrl(item: ClosedTab) fun handleCopyUrl(item: RecoverableTab)
fun handleShare(item: ClosedTab) fun handleShare(item: RecoverableTab)
fun handleNavigateToHistory() fun handleNavigateToHistory()
fun handleRestore(item: ClosedTab) fun handleRestore(item: RecoverableTab)
} }
class DefaultRecentlyClosedController( class DefaultRecentlyClosedController(
private val navController: NavController, private val navController: NavController,
private val store: BrowserStore, private val store: BrowserStore,
private val sessionManager: SessionManager, private val tabsUseCases: TabsUseCases,
private val resources: Resources, private val resources: Resources,
private val snackbar: FenixSnackbar, private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager, private val clipboardManager: ClipboardManager,
private val activity: HomeActivity, private val activity: HomeActivity,
private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit private val openToBrowser: (item: RecoverableTab, mode: BrowsingMode?) -> Unit
) : RecentlyClosedController { ) : RecentlyClosedController {
override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) { override fun handleOpen(item: RecoverableTab, mode: BrowsingMode?) {
openToBrowser(item, mode) openToBrowser(item, mode)
} }
override fun handleDeleteOne(tab: ClosedTab) { override fun handleDeleteOne(tab: RecoverableTab) {
store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab)) store.dispatch(RecentlyClosedAction.RemoveClosedTabAction(tab))
} }
@ -55,7 +54,7 @@ class DefaultRecentlyClosedController(
) )
} }
override fun handleCopyUrl(item: ClosedTab) { override fun handleCopyUrl(item: RecoverableTab) {
val urlClipData = ClipData.newPlainText(item.url, item.url) val urlClipData = ClipData.newPlainText(item.url, item.url)
clipboardManager.setPrimaryClip(urlClipData) clipboardManager.setPrimaryClip(urlClipData)
with(snackbar) { with(snackbar) {
@ -64,7 +63,7 @@ class DefaultRecentlyClosedController(
} }
} }
override fun handleShare(item: ClosedTab) { override fun handleShare(item: RecoverableTab) {
navController.navigate( navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalShareFragment( RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title)) data = arrayOf(ShareData(url = item.url, title = item.title))
@ -72,15 +71,15 @@ class DefaultRecentlyClosedController(
) )
} }
override fun handleRestore(item: ClosedTab) { override fun handleRestore(item: RecoverableTab) {
item.restoreTab( tabsUseCases.restore(item)
store,
sessionManager, store.dispatch(
onTabRestored = { RecentlyClosedAction.RemoveClosedTabAction(item)
activity.openToBrowser( )
from = BrowserDirection.FromRecentlyClosed
) activity.openToBrowser(
} from = BrowserDirection.FromRecentlyClosed
) )
} }
} }

@ -18,7 +18,7 @@ import kotlinx.android.synthetic.main.fragment_recently_closed_tabs.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.lib.state.ext.consumeFrom import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.lib.state.ext.flowScoped import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
@ -34,7 +34,7 @@ import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.library.LibraryPageFragment import org.mozilla.fenix.library.LibraryPageFragment
@Suppress("TooManyFunctions") @Suppress("TooManyFunctions")
class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() { class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
@ -82,7 +82,7 @@ class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
navController = findNavController(), navController = findNavController(),
store = requireComponents.core.store, store = requireComponents.core.store,
activity = activity as HomeActivity, activity = activity as HomeActivity,
sessionManager = requireComponents.core.sessionManager, tabsUseCases = requireComponents.useCases.tabsUseCases,
resources = requireContext().resources, resources = requireContext().resources,
snackbar = FenixSnackbar.make( snackbar = FenixSnackbar.make(
view = requireActivity().getRootView()!!, view = requireActivity().getRootView()!!,
@ -104,7 +104,7 @@ class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
_recentlyClosedFragmentView = null _recentlyClosedFragmentView = null
} }
private fun openItem(tab: ClosedTab, mode: BrowsingMode? = null) { private fun openItem(tab: RecoverableTab, mode: BrowsingMode? = null) {
mode?.let { (activity as HomeActivity).browsingModeManager.mode = it } mode?.let { (activity as HomeActivity).browsingModeManager.mode = it }
(activity as HomeActivity).openToBrowserAndLoad( (activity as HomeActivity).openToBrowserAndLoad(
@ -131,5 +131,5 @@ class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
} }
} }
override val selectedItems: Set<ClosedTab> = setOf() override val selectedItems: Set<RecoverableTab> = setOf()
} }

@ -4,7 +4,7 @@
package org.mozilla.fenix.library.recentlyclosed package org.mozilla.fenix.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.browsingmode.BrowsingMode
/** /**
@ -14,27 +14,27 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
class RecentlyClosedFragmentInteractor( class RecentlyClosedFragmentInteractor(
private val recentlyClosedController: RecentlyClosedController private val recentlyClosedController: RecentlyClosedController
) : RecentlyClosedInteractor { ) : RecentlyClosedInteractor {
override fun restore(item: ClosedTab) { override fun restore(item: RecoverableTab) {
recentlyClosedController.handleRestore(item) recentlyClosedController.handleRestore(item)
} }
override fun onCopyPressed(item: ClosedTab) { override fun onCopyPressed(item: RecoverableTab) {
recentlyClosedController.handleCopyUrl(item) recentlyClosedController.handleCopyUrl(item)
} }
override fun onSharePressed(item: ClosedTab) { override fun onSharePressed(item: RecoverableTab) {
recentlyClosedController.handleShare(item) recentlyClosedController.handleShare(item)
} }
override fun onOpenInNormalTab(item: ClosedTab) { override fun onOpenInNormalTab(item: RecoverableTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Normal) recentlyClosedController.handleOpen(item, BrowsingMode.Normal)
} }
override fun onOpenInPrivateTab(item: ClosedTab) { override fun onOpenInPrivateTab(item: RecoverableTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Private) recentlyClosedController.handleOpen(item, BrowsingMode.Private)
} }
override fun onDeleteOne(tab: ClosedTab) { override fun onDeleteOne(tab: RecoverableTab) {
recentlyClosedController.handleDeleteOne(tab) recentlyClosedController.handleDeleteOne(tab)
} }

@ -4,7 +4,7 @@
package org.mozilla.fenix.library.recentlyclosed package org.mozilla.fenix.library.recentlyclosed
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import mozilla.components.lib.state.Action import mozilla.components.lib.state.Action
import mozilla.components.lib.state.State import mozilla.components.lib.state.State
import mozilla.components.lib.state.Store import mozilla.components.lib.state.Store
@ -23,14 +23,14 @@ class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
* `RecentlyClosedFragmentState` through the reducer. * `RecentlyClosedFragmentState` through the reducer.
*/ */
sealed class RecentlyClosedFragmentAction : Action { sealed class RecentlyClosedFragmentAction : Action {
data class Change(val list: List<ClosedTab>) : RecentlyClosedFragmentAction() data class Change(val list: List<RecoverableTab>) : RecentlyClosedFragmentAction()
} }
/** /**
* The state for the Recently Closed Screen * The state for the Recently Closed Screen
* @property items List of recently closed tabs to display * @property items List of recently closed tabs to display
*/ */
data class RecentlyClosedFragmentState(val items: List<ClosedTab> = emptyList()) : State data class RecentlyClosedFragmentState(val items: List<RecoverableTab> = emptyList()) : State
/** /**
* The RecentlyClosedFragmentState Reducer. * The RecentlyClosedFragmentState Reducer.

@ -12,7 +12,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.component_recently_closed.* import kotlinx.android.synthetic.main.component_recently_closed.*
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.R import org.mozilla.fenix.R
interface RecentlyClosedInteractor { interface RecentlyClosedInteractor {
@ -21,7 +21,7 @@ interface RecentlyClosedInteractor {
* *
* @param item the tapped item to restore. * @param item the tapped item to restore.
*/ */
fun restore(item: ClosedTab) fun restore(item: RecoverableTab)
/** /**
* Called when the view more history option is tapped. * Called when the view more history option is tapped.
@ -33,35 +33,35 @@ interface RecentlyClosedInteractor {
* *
* @param item the recently closed tab item to copy the URL from * @param item the recently closed tab item to copy the URL from
*/ */
fun onCopyPressed(item: ClosedTab) fun onCopyPressed(item: RecoverableTab)
/** /**
* Opens the share sheet for a recently closed tab item. * Opens the share sheet for a recently closed tab item.
* *
* @param item the recently closed tab item to share * @param item the recently closed tab item to share
*/ */
fun onSharePressed(item: ClosedTab) fun onSharePressed(item: RecoverableTab)
/** /**
* Opens a recently closed tab item in a new tab. * Opens a recently closed tab item in a new tab.
* *
* @param item the recently closed tab item to open in a new tab * @param item the recently closed tab item to open in a new tab
*/ */
fun onOpenInNormalTab(item: ClosedTab) fun onOpenInNormalTab(item: RecoverableTab)
/** /**
* Opens a recently closed tab item in a private tab. * Opens a recently closed tab item in a private tab.
* *
* @param item the recently closed tab item to open in a private tab * @param item the recently closed tab item to open in a private tab
*/ */
fun onOpenInPrivateTab(item: ClosedTab) fun onOpenInPrivateTab(item: RecoverableTab)
/** /**
* Deletes one recently closed tab item. * Deletes one recently closed tab item.
* *
* @param item the recently closed tab item to delete. * @param tab the recently closed tab item to delete.
*/ */
fun onDeleteOne(tab: ClosedTab) fun onDeleteOne(tab: RecoverableTab)
} }
/** /**
@ -102,7 +102,7 @@ class RecentlyClosedFragmentView(
} }
} }
fun update(items: List<ClosedTab>) { fun update(items: List<RecoverableTab>) {
recently_closed_empty_view.isVisible = items.isEmpty() recently_closed_empty_view.isVisible = items.isEmpty()
recently_closed_list.isVisible = items.isNotEmpty() recently_closed_list.isVisible = items.isNotEmpty()
recentlyClosedAdapter.submitList(items) recentlyClosedAdapter.submitList(items)

@ -7,7 +7,7 @@ package org.mozilla.fenix.library.recentlyclosed
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.history_list_item.view.* import kotlinx.android.synthetic.main.history_list_item.view.*
import mozilla.components.browser.state.state.ClosedTab import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.library.history.HistoryItemMenu import org.mozilla.fenix.library.history.HistoryItemMenu
import org.mozilla.fenix.utils.Do import org.mozilla.fenix.utils.Do
@ -17,14 +17,14 @@ class RecentlyClosedItemViewHolder(
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor
) : RecyclerView.ViewHolder(view) { ) : RecyclerView.ViewHolder(view) {
private var item: ClosedTab? = null private var item: RecoverableTab? = null
init { init {
setupMenu() setupMenu()
} }
fun bind( fun bind(
item: ClosedTab item: RecoverableTab
) { ) {
itemView.history_layout.titleView.text = itemView.history_layout.titleView.text =
if (item.title.isNotEmpty()) item.title else item.url if (item.title.isNotEmpty()) item.title else item.url

@ -0,0 +1,76 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.perf
import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import org.mozilla.fenix.ext.getAndIncrementNoOverflow
import java.lang.reflect.Modifier.PRIVATE
import java.util.concurrent.atomic.AtomicInteger
private val classPrefixList = arrayOf(
"android.widget.",
"android.webkit.",
"android.app."
)
/**
* Counts the number of inflations fenix does. This class behaves only as an inflation counter since
* it takes the `inflater` that is given by the base system. This is done in order not to change
* the behavior of the app since all we want to do is count the inflations done.
*
*/
open class PerformanceInflater(
inflater: LayoutInflater,
context: Context
) : LayoutInflater(
inflater,
context
) {
override fun cloneInContext(newContext: Context?): LayoutInflater {
return PerformanceInflater(this, newContext!!)
}
override fun inflate(resource: Int, root: ViewGroup?, attachToRoot: Boolean): View {
InflationCounter.inflationCount.getAndIncrementNoOverflow()
return super.inflate(resource, root, attachToRoot)
}
/**
* This code was taken from the PhoneLayoutInflater.java located in the android source code
* (Similarly, AsyncLayoutInflater implements it the exact same way too which can be found in the
* Android Framework). This piece of code was taken from the other inflaters implemented by Android
* since we do not want to change the inflater behavior except to count the number of inflations
* that our app is doing for performance purposes. Looking at the `super.OnCreateView(name, attrs)`,
* it hardcodes the prefix as "android.view." this means that a xml element such as
* ImageButton will crash the app using android.view.ImageButton. This method only works with
* XML tag that contains no prefix. This means that views such as androidx.recyclerview... will not
* work with this method.
*/
@Suppress("EmptyCatchBlock")
@Throws(ClassNotFoundException::class)
override fun onCreateView(name: String?, attrs: AttributeSet?): View? {
for (prefix in classPrefixList) {
try {
val view = createView(name, prefix, attrs)
if (view != null) {
return view
}
} catch (e: ClassNotFoundException) {
// We want the super class to inflate if ever the view can't be inflated here
}
}
return super.onCreateView(name, attrs)
}
}
@VisibleForTesting(otherwise = PRIVATE)
object InflationCounter {
val inflationCount = AtomicInteger(0)
}

@ -12,10 +12,9 @@ import android.text.SpannableString
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController import androidx.navigation.NavController
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.ktx.kotlin.isUrl import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
@ -40,7 +39,6 @@ interface SearchController {
fun handleSearchTermsTapped(searchTerms: String) fun handleSearchTermsTapped(searchTerms: String)
fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine) fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine)
fun handleClickSearchEngineSettings() fun handleClickSearchEngineSettings()
fun handleExistingSessionSelected(session: Session)
fun handleExistingSessionSelected(tabId: String) fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked() fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded() fun handleCameraPermissionsNeeded()
@ -49,8 +47,8 @@ interface SearchController {
@Suppress("TooManyFunctions", "LongParameterList") @Suppress("TooManyFunctions", "LongParameterList")
class SearchDialogController( class SearchDialogController(
private val activity: HomeActivity, private val activity: HomeActivity,
private val sessionManager: SessionManager,
private val store: BrowserStore, private val store: BrowserStore,
private val tabsUseCases: TabsUseCases,
private val fragmentStore: SearchFragmentStore, private val fragmentStore: SearchFragmentStore,
private val navController: NavController, private val navController: NavController,
private val settings: Settings, private val settings: Settings,
@ -74,12 +72,12 @@ class SearchDialogController(
navController.navigateSafe(R.id.searchDialogFragment, directions) navController.navigateSafe(R.id.searchDialogFragment, directions)
} }
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO)) "moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO))
else -> if (url.isNotBlank()) { else ->
openSearchOrUrl(url) if (url.isNotBlank()) {
} else { openSearchOrUrl(url)
dismissDialog() }
}
} }
dismissDialog()
} }
private fun openSearchOrUrl(url: String) { private fun openSearchOrUrl(url: String) {
@ -199,21 +197,16 @@ class SearchDialogController(
navController.navigateSafe(R.id.searchDialogFragment, directions) navController.navigateSafe(R.id.searchDialogFragment, directions)
} }
override fun handleExistingSessionSelected(session: Session) { override fun handleExistingSessionSelected(tabId: String) {
clearToolbarFocus() clearToolbarFocus()
sessionManager.select(session)
tabsUseCases.selectTab(tabId)
activity.openToBrowser( activity.openToBrowser(
from = BrowserDirection.FromSearchDialog from = BrowserDirection.FromSearchDialog
) )
} }
override fun handleExistingSessionSelected(tabId: String) {
val session = sessionManager.findSessionById(tabId)
if (session != null) {
handleExistingSessionSelected(session)
}
}
/** /**
* Creates and shows an [AlertDialog] when camera permissions are needed. * Creates and shows an [AlertDialog] when camera permissions are needed.
* *

@ -141,8 +141,8 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
interactor = SearchDialogInteractor( interactor = SearchDialogInteractor(
SearchDialogController( SearchDialogController(
activity = activity, activity = activity,
sessionManager = requireComponents.core.sessionManager,
store = requireComponents.core.store, store = requireComponents.core.store,
tabsUseCases = requireComponents.useCases.tabsUseCases,
fragmentStore = store, fragmentStore = store,
navController = findNavController(), navController = findNavController(),
settings = requireContext().settings(), settings = requireContext().settings(),
@ -313,23 +313,26 @@ class SearchDialogFragment : AppCompatDialogFragment(), UserInteractionHandler {
} }
consumeFrom(store) { consumeFrom(store) {
val shouldShowAwesomebar = /*
!firstUpdate && * firstUpdate is used to make sure we keep the awesomebar hidden on the first run
it.query.isNotBlank() || * of the searchFragmentDialog. We only turn it false after the user has changed the
it.showSearchShortcuts * query as consumeFrom may run several times on fragment start due to state updates.
* */
awesome_bar?.visibility = if (shouldShowAwesomebar) View.VISIBLE else View.INVISIBLE if (it.url != it.query) firstUpdate = false
awesome_bar?.visibility = if (shouldShowAwesomebar(it)) View.VISIBLE else View.INVISIBLE
updateSearchSuggestionsHintVisibility(it) updateSearchSuggestionsHintVisibility(it)
updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url) updateClipboardSuggestion(it, requireContext().components.clipboardHandler.url)
updateToolbarContentDescription(it) updateToolbarContentDescription(it)
updateSearchShortcutsIcon(it) updateSearchShortcutsIcon(it)
toolbarView.update(it) toolbarView.update(it)
awesomeBarView.update(it) awesomeBarView.update(it)
firstUpdate = false
addVoiceSearchButton(it) addVoiceSearchButton(it)
} }
} }
private fun shouldShowAwesomebar(searchFragmentState: SearchFragmentState) =
!firstUpdate && searchFragmentState.query.isNotBlank() || searchFragmentState.showSearchShortcuts
private fun updateAccessibilityTraversalOrder() { private fun updateAccessibilityTraversalOrder() {
val searchWrapperId = search_wrapper.id val searchWrapperId = search_wrapper.id
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {

@ -4,7 +4,6 @@
package org.mozilla.fenix.search package org.mozilla.fenix.search
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor import org.mozilla.fenix.search.awesomebar.AwesomeBarInteractor
import org.mozilla.fenix.search.toolbar.ToolbarInteractor import org.mozilla.fenix.search.toolbar.ToolbarInteractor
@ -50,10 +49,6 @@ class SearchDialogInteractor(
searchController.handleClickSearchEngineSettings() searchController.handleClickSearchEngineSettings()
} }
override fun onExistingSessionSelected(session: Session) {
searchController.handleExistingSessionSelected(session)
}
override fun onExistingSessionSelected(tabId: String) { override fun onExistingSessionSelected(tabId: String) {
searchController.handleExistingSessionSelected(tabId) searchController.handleExistingSessionSelected(tabId)
} }

@ -4,7 +4,6 @@
package org.mozilla.fenix.search.awesomebar package org.mozilla.fenix.search.awesomebar
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.search.SearchEngine import mozilla.components.browser.state.search.SearchEngine
/** /**
@ -36,11 +35,6 @@ interface AwesomeBarInteractor {
*/ */
fun onClickSearchEngineSettings() fun onClickSearchEngineSettings()
/**
* Called whenever an existing session is selected from the sessionSuggestionProvider
*/
fun onExistingSessionSelected(session: Session)
/** /**
* Called whenever an existing session is selected from the sessionSuggestionProvider * Called whenever an existing session is selected from the sessionSuggestionProvider
*/ */

@ -86,7 +86,7 @@ class AwesomeBarView(
private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase { private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase {
override fun invoke(session: Session) { override fun invoke(session: Session) {
interactor.onExistingSessionSelected(session) interactor.onExistingSessionSelected(session.id)
} }
override fun invoke(tabId: String) { override fun invoke(tabId: String) {

@ -59,8 +59,9 @@ class InContentTelemetry(private val metrics: MetricController) : BaseSearchTele
// For Bing if it didn't have a valid cookie and for all the other search engines // For Bing if it didn't have a valid cookie and for all the other search engines
if (resultNotComputedFromCookies(trackKey) && hasValidCode(code, provider)) { if (resultNotComputedFromCookies(trackKey) && hasValidCode(code, provider)) {
val channel = uri.getQueryParameter(CHANNEL_KEY)
val type = getSapType(provider.followOnParams, paramSet) val type = getSapType(provider.followOnParams, paramSet)
trackKey = TrackKeyInfo(provider.name, type, code) trackKey = TrackKeyInfo(provider.name, type, code, channel)
} }
} }
@ -145,5 +146,7 @@ class InContentTelemetry(private val metrics: MetricController) : BaseSearchTele
private const val SEARCH_TYPE_ORGANIC = "organic" private const val SEARCH_TYPE_ORGANIC = "organic"
private const val SEARCH_TYPE_SAP = "sap" private const val SEARCH_TYPE_SAP = "sap"
private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on" private const val SEARCH_TYPE_SAP_FOLLOW_ON = "sap-follow-on"
private const val CHANNEL_KEY = "channel"
} }
} }

@ -9,11 +9,15 @@ import java.util.Locale
internal data class TrackKeyInfo( internal data class TrackKeyInfo(
var providerName: String, var providerName: String,
var type: String, var type: String,
var code: String? var code: String?,
var channel: String? = null
) { ) {
fun createTrackKey(): String { fun createTrackKey(): String {
return "${providerName.toLowerCase(Locale.ROOT)}.in-content" + return "${providerName.toLowerCase(Locale.ROOT)}.in-content" +
".${type.toLowerCase(Locale.ROOT)}" + ".${type.toLowerCase(Locale.ROOT)}" +
".${code?.toLowerCase(Locale.ROOT) ?: "none"}" ".${code?.toLowerCase(Locale.ROOT) ?: "none"}" +
if (!channel?.toLowerCase(Locale.ROOT).isNullOrBlank())
".${channel?.toLowerCase(Locale.ROOT)}"
else ""
} }
} }

@ -33,8 +33,8 @@ class PrivateNotificationService : AbstractPrivateNotificationService() {
override fun NotificationCompat.Builder.buildNotification() { override fun NotificationCompat.Builder.buildNotification() {
setSmallIcon(R.drawable.ic_private_browsing) setSmallIcon(R.drawable.ic_private_browsing)
setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name))) setContentTitle(applicationContext.getString(R.string.app_name_private_4, getString(R.string.app_name)))
setContentText(getString(R.string.notification_pbm_delete_text_2)) setContentText(applicationContext.getString(R.string.notification_pbm_delete_text_2))
color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color) color = ContextCompat.getColor(this@PrivateNotificationService, R.color.pbm_notification_color)
} }

@ -19,23 +19,28 @@ fun SitePermissions.toggle(featurePhone: PhoneFeature): SitePermissions {
} }
fun SitePermissions.get(field: PhoneFeature) = when (field) { fun SitePermissions.get(field: PhoneFeature) = when (field) {
PhoneFeature.AUTOPLAY ->
throw IllegalAccessException("AUTOPLAY can't be accessed via get try " +
"using AUTOPLAY_AUDIBLE and AUTOPLAY_INAUDIBLE")
PhoneFeature.CAMERA -> camera PhoneFeature.CAMERA -> camera
PhoneFeature.LOCATION -> location PhoneFeature.LOCATION -> location
PhoneFeature.MICROPHONE -> microphone PhoneFeature.MICROPHONE -> microphone
PhoneFeature.NOTIFICATION -> notification PhoneFeature.NOTIFICATION -> notification
PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible.toStatus()
PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible.toStatus()
PhoneFeature.PERSISTENT_STORAGE -> localStorage PhoneFeature.PERSISTENT_STORAGE -> localStorage
PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess
} }
fun SitePermissions.update(field: PhoneFeature, value: SitePermissions.Status) = when (field) { fun SitePermissions.update(field: PhoneFeature, value: SitePermissions.Status) = when (field) {
PhoneFeature.AUTOPLAY -> throw IllegalAccessException("AUTOPLAY can't be accessed via update " +
"try using AUTOPLAY_AUDIBLE and AUTOPLAY_INAUDIBLE")
PhoneFeature.CAMERA -> copy(camera = value) PhoneFeature.CAMERA -> copy(camera = value)
PhoneFeature.LOCATION -> copy(location = value) PhoneFeature.LOCATION -> copy(location = value)
PhoneFeature.MICROPHONE -> copy(microphone = value) PhoneFeature.MICROPHONE -> copy(microphone = value)
PhoneFeature.NOTIFICATION -> copy(notification = value) PhoneFeature.NOTIFICATION -> copy(notification = value)
PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value) PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value.toAutoplayStatus())
PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value) PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value.toAutoplayStatus())
PhoneFeature.PERSISTENT_STORAGE -> copy(localStorage = value) PhoneFeature.PERSISTENT_STORAGE -> copy(localStorage = value)
PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> copy(mediaKeySystemAccess = value) PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> copy(mediaKeySystemAccess = value)
} }

@ -29,6 +29,7 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
LOCATION(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)), LOCATION(arrayOf(ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION)),
MICROPHONE(arrayOf(RECORD_AUDIO)), MICROPHONE(arrayOf(RECORD_AUDIO)),
NOTIFICATION(emptyArray()), NOTIFICATION(emptyArray()),
AUTOPLAY(emptyArray()),
AUTOPLAY_AUDIBLE(emptyArray()), AUTOPLAY_AUDIBLE(emptyArray()),
AUTOPLAY_INAUDIBLE(emptyArray()), AUTOPLAY_INAUDIBLE(emptyArray()),
PERSISTENT_STORAGE(emptyArray()), PERSISTENT_STORAGE(emptyArray()),
@ -82,7 +83,8 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
NOTIFICATION -> context.getString(R.string.preference_phone_feature_notification) NOTIFICATION -> context.getString(R.string.preference_phone_feature_notification)
PERSISTENT_STORAGE -> context.getString(R.string.preference_phone_feature_persistent_storage) PERSISTENT_STORAGE -> context.getString(R.string.preference_phone_feature_persistent_storage)
MEDIA_KEY_SYSTEM_ACCESS -> context.getString(R.string.preference_phone_feature_media_key_system_access) MEDIA_KEY_SYSTEM_ACCESS -> context.getString(R.string.preference_phone_feature_media_key_system_access)
AUTOPLAY_AUDIBLE, AUTOPLAY_INAUDIBLE -> context.getString(R.string.preference_browser_feature_autoplay) AUTOPLAY, AUTOPLAY_AUDIBLE, AUTOPLAY_INAUDIBLE ->
context.getString(R.string.preference_browser_feature_autoplay)
} }
} }
@ -97,6 +99,7 @@ enum class PhoneFeature(val androidPermissionsList: Array<String>) : Parcelable
LOCATION -> R.string.pref_key_phone_feature_location LOCATION -> R.string.pref_key_phone_feature_location
MICROPHONE -> R.string.pref_key_phone_feature_microphone MICROPHONE -> R.string.pref_key_phone_feature_microphone
NOTIFICATION -> R.string.pref_key_phone_feature_notification NOTIFICATION -> R.string.pref_key_phone_feature_notification
AUTOPLAY -> R.string.pref_key_browser_feature_autoplay_audible
AUTOPLAY_AUDIBLE -> R.string.pref_key_browser_feature_autoplay_audible AUTOPLAY_AUDIBLE -> R.string.pref_key_browser_feature_autoplay_audible
AUTOPLAY_INAUDIBLE -> R.string.pref_key_browser_feature_autoplay_inaudible AUTOPLAY_INAUDIBLE -> R.string.pref_key_browser_feature_autoplay_inaudible
PERSISTENT_STORAGE -> R.string.pref_key_browser_feature_persistent_storage PERSISTENT_STORAGE -> R.string.pref_key_browser_feature_persistent_storage

@ -5,6 +5,8 @@
package org.mozilla.fenix.settings package org.mozilla.fenix.settings
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.app.role.RoleManager
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -35,7 +37,6 @@ import mozilla.components.support.ktx.android.content.getColorFromAttr
import mozilla.components.support.ktx.android.view.showKeyboard import mozilla.components.support.ktx.android.view.showKeyboard
import org.mozilla.fenix.BrowserDirection import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.components.metrics.Event
@ -359,8 +360,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
val debuggingKey = getPreferenceKey(R.string.pref_key_remote_debugging) val debuggingKey = getPreferenceKey(R.string.pref_key_remote_debugging)
val preferencePrivateBrowsing = val preferencePrivateBrowsing =
requirePreference<Preference>(R.string.pref_key_private_browsing) requirePreference<Preference>(R.string.pref_key_private_browsing)
val preferenceExternalDownloadManager =
requirePreference<Preference>(R.string.pref_key_external_download_manager)
val preferenceLeakCanary = findPreference<Preference>(leakKey) val preferenceLeakCanary = findPreference<Preference>(leakKey)
val preferenceRemoteDebugging = findPreference<Preference>(debuggingKey) val preferenceRemoteDebugging = findPreference<Preference>(debuggingKey)
val preferenceMakeDefaultBrowser = val preferenceMakeDefaultBrowser =
@ -380,7 +379,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
} }
} }
preferenceExternalDownloadManager.isVisible = FeatureFlags.externalDownloadManager
preferenceRemoteDebugging?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M preferenceRemoteDebugging?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue -> preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue ->
preference.context.settings().preferences.edit() preference.context.settings().preferences.edit()
@ -426,28 +424,66 @@ class SettingsFragment : PreferenceFragmentCompat() {
setupAmoCollectionOverridePreference(requireContext().settings()) setupAmoCollectionOverridePreference(requireContext().settings())
} }
/**
* For >=Q -> Use new RoleManager API to show in-app browser switching dialog.
* For <Q && >=N -> Navigate user to Android Default Apps Settings.
* For <N -> Open sumo page to show user how to change default app.
*/
private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener { private fun getClickListenerForMakeDefaultBrowser(): Preference.OnPreferenceClickListener {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { return when {
Preference.OnPreferenceClickListener { Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS) Preference.OnPreferenceClickListener {
startActivity(intent) requireContext().getSystemService(RoleManager::class.java).also {
true if (!it.isRoleHeld(RoleManager.ROLE_BROWSER)) {
startActivityForResult(it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), 0)
} else {
navigateUserToDefaultAppsSettings()
}
}
true
}
} }
} else { Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
(activity as HomeActivity).openToBrowserAndLoad( navigateUserToDefaultAppsSettings()
searchTermOrURL = SupportUtils.getSumoURLForTopic( true
requireContext(), }
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER }
), else -> {
newTab = true, Preference.OnPreferenceClickListener {
from = BrowserDirection.FromSettings (activity as HomeActivity).openToBrowserAndLoad(
) searchTermOrURL = SupportUtils.getSumoURLForTopic(
true requireContext(),
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
),
newTab = true,
from = BrowserDirection.FromSettings
)
true
}
} }
} }
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
/*
If role manager doesn't show in-app browser changing dialog for a reason, navigate user to
Default Apps Settings.
*/
if (resultCode == Activity.RESULT_CANCELED && requestCode == 0) {
navigateUserToDefaultAppsSettings()
}
}
private fun navigateUserToDefaultAppsSettings() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
startActivity(intent)
}
}
private fun updateMakeDefaultBrowserPreference() { private fun updateMakeDefaultBrowserPreference() {
requirePreference<DefaultBrowserPreference>(R.string.pref_key_make_default_browser).updateSwitch() requirePreference<DefaultBrowserPreference>(R.string.pref_key_make_default_browser).updateSwitch()
} }

@ -32,6 +32,8 @@ object SupportUtils {
"?e=&p=AyIGZRprFDJWWA1FBCVbV0IUWVALHFRBEwQAQB1AWQkFVUVXfFkAF14lRFRbJXstVWR3WQ1rJ08AZnhS" + "?e=&p=AyIGZRprFDJWWA1FBCVbV0IUWVALHFRBEwQAQB1AWQkFVUVXfFkAF14lRFRbJXstVWR3WQ1rJ08AZnhS" +
"HDJBYh4LZR9eEAMUBlccWCUBEQZRGFoXCxc3ZRteJUl8BmUZWhQ" + "HDJBYh4LZR9eEAMUBlccWCUBEQZRGFoXCxc3ZRteJUl8BmUZWhQ" +
"AEwdRGF0cMhIAVB5ZFAETBVAaXRwyFQdcKydLSUpaCEtYFAIXN2UrWCUyIgdVK1slXVZaCCtZFAMWDg%3D%3D" "AEwdRGF0cMhIAVB5ZFAETBVAaXRwyFQdcKydLSUpaCEtYFAIXN2UrWCUyIgdVK1slXVZaCCtZFAMWDg%3D%3D"
const val GOOGLE_US_URL = "https://www.google.com/webhp?client=firefox-b-1-m&channel=ts"
const val GOOGLE_XX_URL = "https://www.google.com/webhp?client=firefox-b-m&channel=ts"
enum class SumoTopic(internal val topicStr: String) { enum class SumoTopic(internal val topicStr: String) {
FENIX_MOVING("sync-delist"), FENIX_MOVING("sync-delist"),

@ -41,6 +41,7 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
private lateinit var customTrackingSelect: DropDownPreference private lateinit var customTrackingSelect: DropDownPreference
private lateinit var customCryptominers: CheckBoxPreference private lateinit var customCryptominers: CheckBoxPreference
private lateinit var customFingerprinters: CheckBoxPreference private lateinit var customFingerprinters: CheckBoxPreference
private lateinit var customRedirectTrackers: CheckBoxPreference
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.tracking_protection_preferences, rootKey) setPreferencesFromResource(R.xml.tracking_protection_preferences, rootKey)
@ -145,6 +146,9 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
customFingerprinters = customFingerprinters =
requirePreference(R.string.pref_key_tracking_protection_custom_fingerprinters) requirePreference(R.string.pref_key_tracking_protection_custom_fingerprinters)
customRedirectTrackers =
requirePreference(R.string.pref_key_tracking_protection_redirect_trackers)
customCookies.onPreferenceChangeListener = object : SharedPreferenceUpdater() { customCookies.onPreferenceChangeListener = object : SharedPreferenceUpdater() {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean { override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
customCookiesSelect.isVisible = !customCookies.isChecked customCookiesSelect.isVisible = !customCookies.isChecked
@ -196,6 +200,14 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
} }
} }
customRedirectTrackers.onPreferenceChangeListener = object : SharedPreferenceUpdater() {
override fun onPreferenceChange(preference: Preference, newValue: Any?): Boolean {
return super.onPreferenceChange(preference, newValue).also {
updateTrackingProtectionPolicy()
}
}
}
updateCustomOptionsVisibility() updateCustomOptionsVisibility()
return radio return radio
@ -218,5 +230,6 @@ class TrackingProtectionFragment : PreferenceFragmentCompat() {
customTrackingSelect.isVisible = isCustomSelected && customTracking.isChecked customTrackingSelect.isVisible = isCustomSelected && customTracking.isChecked
customCryptominers.isVisible = isCustomSelected customCryptominers.isVisible = isCustomSelected
customFingerprinters.isVisible = isCustomSelected customFingerprinters.isVisible = isCustomSelected
customRedirectTrackers.isVisible = isCustomSelected
} }
} }

@ -12,6 +12,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
/** /**
* Processes incoming intents and sends them to the corresponding activity. * Processes incoming intents and sends them to the corresponding activity.
@ -27,7 +28,13 @@ class AuthIntentReceiverActivity : Activity() {
// assumes it is not. If it's null, then we make a new one and open // assumes it is not. If it's null, then we make a new one and open
// the HomeActivity. // the HomeActivity.
val intent = intent?.let { Intent(intent) } ?: Intent() val intent = intent?.let { Intent(intent) } ?: Intent()
components.intentProcessors.customTabIntentProcessor.process(intent)
if (settings().lastKnownMode.isPrivate) {
components.intentProcessors.privateCustomTabIntentProcessor.process(intent)
} else {
components.intentProcessors.customTabIntentProcessor.process(intent)
}
intent.setClassName(applicationContext, AuthCustomTabActivity::class.java.name) intent.setClassName(applicationContext, AuthCustomTabActivity::class.java.name)
intent.putExtra(HomeActivity.OPEN_TO_BROWSER, true) intent.putExtra(HomeActivity.OPEN_TO_BROWSER, true)

@ -11,7 +11,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import org.mozilla.fenix.addons.runIfFragmentIsAttached
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -21,6 +20,7 @@ import kotlinx.coroutines.launch
import mozilla.components.service.fxa.manager.FxaAccountManager import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R import org.mozilla.fenix.R
import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.ext.runIfFragmentIsAttached
class SignOutFragment : AppCompatDialogFragment() { class SignOutFragment : AppCompatDialogFragment() {
private lateinit var accountManager: FxaAccountManager private lateinit var accountManager: FxaAccountManager

@ -24,6 +24,7 @@ fun deleteAndQuit(activity: Activity, coroutineScope: CoroutineScope, snackbar:
val settings = activity.settings() val settings = activity.settings()
val controller = DefaultDeleteBrowsingDataController( val controller = DefaultDeleteBrowsingDataController(
activity.components.useCases.tabsUseCases.removeAllTabs, activity.components.useCases.tabsUseCases.removeAllTabs,
activity.components.useCases.downloadUseCases.removeAllDownloads,
activity.components.core.historyStorage, activity.components.core.historyStorage,
activity.components.core.permissionStorage, activity.components.core.permissionStorage,
activity.components.core.store, activity.components.core.store,
@ -61,5 +62,6 @@ private suspend fun DeleteBrowsingDataController.deleteType(type: DeleteBrowsing
DeleteBrowsingDataOnQuitType.PERMISSIONS -> withContext(IO) { DeleteBrowsingDataOnQuitType.PERMISSIONS -> withContext(IO) {
deleteSitePermissions() deleteSitePermissions()
} }
DeleteBrowsingDataOnQuitType.DOWNLOADS -> deleteDownloads()
} }
} }

@ -7,10 +7,12 @@ package org.mozilla.fenix.settings.deletebrowsingdata
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import mozilla.components.browser.icons.BrowserIcons import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.RecentlyClosedAction import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.tabs.TabsUseCases import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.components.PermissionStorage import org.mozilla.fenix.components.PermissionStorage
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
@ -21,10 +23,12 @@ interface DeleteBrowsingDataController {
suspend fun deleteCookies() suspend fun deleteCookies()
suspend fun deleteCachedFiles() suspend fun deleteCachedFiles()
suspend fun deleteSitePermissions() suspend fun deleteSitePermissions()
suspend fun deleteDownloads()
} }
class DefaultDeleteBrowsingDataController( class DefaultDeleteBrowsingDataController(
private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase, private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
private val removeAllDownloads: DownloadsUseCases.RemoveAllDownloadsUseCase,
private val historyStorage: HistoryStorage, private val historyStorage: HistoryStorage,
private val permissionStorage: PermissionStorage, private val permissionStorage: PermissionStorage,
private val store: BrowserStore, private val store: BrowserStore,
@ -43,6 +47,7 @@ class DefaultDeleteBrowsingDataController(
withContext(coroutineContext) { withContext(coroutineContext) {
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything() historyStorage.deleteEverything()
store.dispatch(EngineAction.PurgeHistoryAction)
iconsStorage.clear() iconsStorage.clear()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
} }
@ -75,4 +80,10 @@ class DefaultDeleteBrowsingDataController(
} }
permissionStorage.deleteAllSitePermissions() permissionStorage.deleteAllSitePermissions()
} }
override suspend fun deleteDownloads() {
withContext(coroutineContext) {
removeAllDownloads.invoke()
}
}
} }

@ -33,7 +33,7 @@ import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.utils.Settings import org.mozilla.fenix.utils.Settings
@SuppressWarnings("TooManyFunctions") @SuppressWarnings("TooManyFunctions", "LargeClass")
class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_data) { class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_data) {
private lateinit var controller: DeleteBrowsingDataController private lateinit var controller: DeleteBrowsingDataController
@ -42,9 +42,11 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val tabsUseCases = requireComponents.useCases.tabsUseCases
val downloadUseCases = requireComponents.useCases.downloadUseCases
controller = DefaultDeleteBrowsingDataController( controller = DefaultDeleteBrowsingDataController(
requireComponents.useCases.tabsUseCases.removeAllTabs, tabsUseCases.removeAllTabs,
downloadUseCases.removeAllDownloads,
requireComponents.core.historyStorage, requireComponents.core.historyStorage,
requireComponents.core.permissionStorage, requireComponents.core.permissionStorage,
requireComponents.core.store, requireComponents.core.store,
@ -67,6 +69,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
R.id.cookies_item -> settings.deleteCookies R.id.cookies_item -> settings.deleteCookies
R.id.cached_files_item -> settings.deleteCache R.id.cached_files_item -> settings.deleteCache
R.id.site_permissions_item -> settings.deleteSitePermissions R.id.site_permissions_item -> settings.deleteSitePermissions
R.id.downloads_item -> settings.deleteDownloads
else -> true else -> true
} }
} }
@ -84,6 +87,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
R.id.cookies_item -> settings.deleteCookies = it.isChecked R.id.cookies_item -> settings.deleteCookies = it.isChecked
R.id.cached_files_item -> settings.deleteCache = it.isChecked R.id.cached_files_item -> settings.deleteCache = it.isChecked
R.id.site_permissions_item -> settings.deleteSitePermissions = it.isChecked R.id.site_permissions_item -> settings.deleteSitePermissions = it.isChecked
R.id.downloads_item -> settings.deleteDownloads = it.isChecked
else -> return else -> return
} }
} }
@ -151,6 +155,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
COOKIES_INDEX -> controller.deleteCookies() COOKIES_INDEX -> controller.deleteCookies()
CACHED_INDEX -> controller.deleteCachedFiles() CACHED_INDEX -> controller.deleteCachedFiles()
PERMS_INDEX -> controller.deleteSitePermissions() PERMS_INDEX -> controller.deleteSitePermissions()
DOWNLOADS_INDEX -> controller.deleteDownloads()
} }
} }
} }
@ -262,7 +267,8 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
fragmentView.browsing_data_item, fragmentView.browsing_data_item,
fragmentView.cookies_item, fragmentView.cookies_item,
fragmentView.cached_files_item, fragmentView.cached_files_item,
fragmentView.site_permissions_item fragmentView.site_permissions_item,
fragmentView.downloads_item
) )
} }
@ -275,5 +281,6 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
private const val COOKIES_INDEX = 2 private const val COOKIES_INDEX = 2
private const val CACHED_INDEX = 3 private const val CACHED_INDEX = 3
private const val PERMS_INDEX = 4 private const val PERMS_INDEX = 4
private const val DOWNLOADS_INDEX = 5
} }
} }

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

Loading…
Cancel
Save