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:
method: rebase
strict: smart
- name: Release automation
- name: Release automation (Old)
conditions:
- base~=releases/.*
- base~=releases[_/].*
- author=github-actions[bot]
# Listing checks manually beause we do not have a "push complete" check yet.
- check-success=build-android-test-debug
@ -57,3 +57,32 @@ pull_request_rules:
strict: smart
delete_head_branch:
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'
resValue "bool", "IS_DEBUG", "false"
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "false"
buildConfigField "String", "AMO_ACCOUNT", "\"mozilla\""
buildConfigField "String", "AMO_COLLECTION", "\"7dfae8669acc4312a65e8ba5553036\""
// This should be the "public" base URL of AMO.
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"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
manifestPlaceholders = [
@ -131,8 +135,8 @@ android {
def deepLinkSchemeValue = "iceraven-debug"
buildConfigField "String", "DEEP_LINK_SCHEME", "\"$deepLinkSchemeValue\""
// Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
}
forkRelease releaseTemplate >> {
buildConfigField "boolean", "USE_RELEASE_VERSIONING", "true"
@ -143,8 +147,8 @@ android {
"deepLinkScheme": deepLinkSchemeValue
]
// Use custom default allowed addon list
buildConfigField "String", "AMO_ACCOUNT", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION", "\"What-I-want-on-Fenix\""
buildConfigField "String", "AMO_COLLECTION_USER", "\"16201230\""
buildConfigField "String", "AMO_COLLECTION_NAME", "\"What-I-want-on-Fenix\""
}
}
@ -466,6 +470,7 @@ dependencies {
implementation Deps.mozilla_browser_menu2
implementation Deps.mozilla_browser_search
implementation Deps.mozilla_browser_session
implementation Deps.mozilla_browser_session_storage
implementation Deps.mozilla_browser_state
implementation Deps.mozilla_browser_storage_sync
implementation Deps.mozilla_browser_tabstray

@ -910,7 +910,7 @@ metrics:
https://github.com/mozilla-mobile/fenix/issues/1607) the value will be
`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
`other` option for the source but it should never enter on this case.
send_in_pings:
@ -2206,6 +2206,36 @@ reader_mode:
- fenix-core@mozilla.com
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:
opened:
type: event
@ -3385,6 +3415,19 @@ top_sites:
notification_emails:
- fenix-core@mozilla.com
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:
type: event
description: |
@ -4075,6 +4118,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity:
- technical
- interaction
@ -4082,7 +4126,7 @@ storage.stats:
- fenix-core@mozilla.com
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-02-01"
expires: "2021-08-01"
app_bytes:
send_in_pings:
- metrics
@ -4099,6 +4143,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity:
- technical
- interaction
@ -4106,7 +4151,7 @@ storage.stats:
- fenix-core@mozilla.com
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-02-01"
expires: "2021-08-01"
cache_bytes:
send_in_pings:
- metrics
@ -4120,6 +4165,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity:
- technical
- interaction
@ -4127,7 +4173,7 @@ storage.stats:
- fenix-core@mozilla.com
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-02-01"
expires: "2021-08-01"
data_dir_bytes:
send_in_pings:
- metrics
@ -4143,6 +4189,7 @@ storage.stats:
- https://github.com/mozilla-mobile/fenix/issues/12802
data_reviews:
- https://github.com/mozilla-mobile/fenix/pull/12876#issuecomment-666770732
- https://github.com/mozilla-mobile/fenix/pull/17704#issue-564299127
data_sensitivity:
- technical
- interaction
@ -4150,7 +4197,7 @@ storage.stats:
- fenix-core@mozilla.com
- perf-android-fe@mozilla.com
- mcomella@mozilla.com
expires: "2021-02-01"
expires: "2021-08-01"
progressive_web_app:
homescreen_tap:
@ -4260,3 +4307,57 @@ tabs:
notification_emails:
- fenix-core@mozilla.com
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 {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int d(...);
}

@ -1,5 +1,10 @@
<html>
<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>
</html>

@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import androidx.preference.PreferenceManager
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.ViewAction
@ -25,11 +26,13 @@ import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import kotlinx.coroutines.runBlocking
import org.hamcrest.CoreMatchers
import org.hamcrest.CoreMatchers.allOf
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.ext.waitNotNull
import org.mozilla.fenix.ui.robots.mDevice
import java.io.File
object TestHelper {
@ -120,4 +123,19 @@ object TestHelper {
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_VIEW_HIERARCHY_DEPTH = 12
private const val EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN = 4
private const val EXPECTED_NUMBER_OF_INFLATION = 12
private val failureMsgStrictMode = getErrorMessage(
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, " +
"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
* on startup.
@ -90,6 +96,8 @@ class StartupExcessiveResourceUseTest {
val actualViewHierarchyDepth = countAndLogViewHierarchyDepth(rootView, 1)
val actualRecyclerViewConstraintLayoutChildren = countRecyclerViewConstraintLayoutChildren(rootView, null)
val actualNumberOfInflations = InflationCounter.inflationCount.get()
assertEquals(failureMsgStrictMode, EXPECTED_SUPPRESSION_COUNT, actualSuppresionCount)
assertEquals(failureMsgRunBlocking, EXPECTED_RUNBLOCKING_COUNT, actualRunBlocking)
assertEquals(failureMsgComponentInit, EXPECTED_COMPONENT_INIT_COUNT, actualComponentInitCount)
@ -99,6 +107,7 @@ class StartupExcessiveResourceUseTest {
EXPECTED_RECYCLER_VIEW_CONSTRAINT_LAYOUT_CHILDREN,
actualRecyclerViewConstraintLayoutChildren
)
assertEquals(failureMsgNumberOfInflation, EXPECTED_NUMBER_OF_INFLATION, actualNumberOfInflations)
}
}

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

@ -52,40 +52,6 @@
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @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
// fun addTabToExistingCollectionTest() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -135,23 +101,6 @@
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @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() {
// createCollection(firstCollectionName)
//
@ -167,20 +116,6 @@
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @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() {
// val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//
@ -199,42 +134,6 @@
//
// @Ignore("Intermittent failures, see: https://github.com/mozilla-mobile/fenix/issues/10587")
// @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() {
// val webPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
//

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

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

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

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

@ -5,25 +5,34 @@
package org.mozilla.fenix.ui
import android.view.View
import androidx.core.net.toUri
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.IdlingRegistry
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import okhttp3.mockwebserver.MockWebServer
import org.junit.After
import org.junit.Before
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.helpers.AndroidAssetDispatcher
import org.mozilla.fenix.helpers.HomeActivityTestRule
import org.mozilla.fenix.helpers.RecyclerViewIdlingResource
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
import org.mozilla.fenix.helpers.TestHelper.deleteDownloadFromStorage
import org.mozilla.fenix.helpers.ViewVisibilityIdlingResource
import org.mozilla.fenix.ui.robots.browserScreen
import org.mozilla.fenix.ui.robots.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.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.
@ -35,6 +44,16 @@ class SmokeTest {
private lateinit var mockWebServer: MockWebServer
private var awesomeBar: ViewVisibilityIdlingResource? = 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
private fun getAwesomebarView(): View? {
@ -48,6 +67,12 @@ class SmokeTest {
@get:Rule
val activityTestRule = HomeActivityTestRule()
@get:Rule
var mGrantPermissions = GrantPermissionRule.grant(
android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
android.Manifest.permission.READ_EXTERNAL_STORAGE
)
@Before
fun setUp() {
mockWebServer = MockWebServer().apply {
@ -59,6 +84,32 @@ class SmokeTest {
@After
fun tearDown() {
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
@ -112,6 +163,7 @@ class SmokeTest {
}
@Test
// Verifies the functionality of the onboarding Start Browsing button
fun startBrowsingButtonTest() {
homeScreen {
verifyStartBrowsingButton()
@ -121,6 +173,13 @@ class SmokeTest {
}
@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() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -141,6 +200,7 @@ class SmokeTest {
}
@Test
// Verifies the list of items in a tab's 3 dot menu
fun verifyPageMainMenuItemsTest() {
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
@Test
// Verifies the History menu opens from a tab's 3 dot menu
fun openMainMenuHistoryItemTest() {
homeScreen {
}.openThreeDotMenu {
@ -163,6 +224,7 @@ class SmokeTest {
// Could be removed when more smoke tests from the Bookmarks category are added
@Test
// Verifies the Bookmarks menu opens from a tab's 3 dot menu
fun openMainMenuBookmarksItemTest() {
homeScreen {
}.openThreeDotMenu {
@ -172,6 +234,7 @@ class SmokeTest {
}
@Test
// Verifies the Synced tabs menu opens from a tab's 3 dot menu
fun openMainMenuSyncedTabsItemTest() {
homeScreen {
}.openThreeDotMenu {
@ -182,6 +245,7 @@ class SmokeTest {
// Could be removed when more smoke tests from the Settings category are added
@Test
// Verifies the Settings menu opens from a tab's 3 dot menu
fun openMainMenuSettingsItemTest() {
homeScreen {
}.openThreeDotMenu {
@ -191,6 +255,7 @@ class SmokeTest {
}
@Test
// Verifies the Find in page option in a tab's 3 dot menu
fun openMainMenuFindInPageTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -203,6 +268,7 @@ class SmokeTest {
}
@Test
// Verifies the Add to top sites option in a tab's 3 dot menu
fun openMainMenuAddTopSiteTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -219,22 +285,31 @@ class SmokeTest {
}
@Test
// Verifies the Add to home screen option in a tab's 3 dot menu
fun mainMenuAddToHomeScreenTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val website = TestAssetHelper.getGenericAsset(mockWebServer, 1)
navigationToolbar {
}.enterURLAndEnterToBrowser(defaultWebPage.url) {
homeScreen {
}.openNavigationToolbar {
}.enterURLAndEnterToBrowser(website.url) {
}.openThreeDotMenu {
}.openAddToHomeScreen {
clickCancelShortcutButton()
}
browserScreen {
}.openThreeDotMenu {
}.openAddToHomeScreen {
verifyShortcutNameField(defaultWebPage.title)
verifyShortcutNameField("Test_Page_1")
addShortcutName("Test Page")
clickAddShortcutButton()
clickAddAutomaticallyButton()
}.openHomeScreenShortcut(defaultWebPage.title) {
verifyPageContent(defaultWebPage.content)
}.openHomeScreenShortcut("Test Page") {
}
}
@Test
// Verifies the Add to collection option in a tab's 3 dot menu
fun openMainMenuAddToCollectionTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -247,6 +322,7 @@ class SmokeTest {
}
@Test
// Verifies the Bookmark button in a tab's 3 dot menu
fun mainMenuBookmarkButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -259,6 +335,7 @@ class SmokeTest {
}
@Test
// Verifies the Share button in a tab's 3 dot menu
fun mainMenuShareButtonTest() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -271,6 +348,7 @@ class SmokeTest {
}
@Test
// Verifies the refresh button in a tab's 3 dot menu
fun mainMenuRefreshButtonTest() {
val refreshWebPage = TestAssetHelper.getRefreshAsset(mockWebServer)
@ -286,6 +364,7 @@ class SmokeTest {
}
@Test
// Turns ETP toggle off from Settings and verifies the ETP shield is not displayed in the nav bar
fun verifyETPShieldNotDisplayedIfOFFGlobally() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -313,6 +392,7 @@ class SmokeTest {
}
@Test
// Verifies changing the default engine from the Search Shortcut menu
fun verifySearchEngineCanBeChangedTemporarilyUsingShortcuts() {
val defaultWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
@ -361,6 +441,7 @@ class SmokeTest {
}
@Test
// Ads a new search engine from the list of custom engines
fun addPredefinedSearchEngineTest() {
homeScreen {
}.openThreeDotMenu {
@ -380,8 +461,9 @@ class SmokeTest {
}
@Test
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
fun toggleSearchSuggestions() {
// Goes through the settings and changes the search suggestion toggle, then verifies it changes.
homeScreen {
}.openNavigationToolbar {
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
// Swipes the nav bar left/right to switch between tabs
fun swipeToSwitchTabTest() {
val firstWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 1)
val secondWebPage = TestAssetHelper.getGenericAsset(mockWebServer, 2)
@ -430,6 +539,7 @@ class SmokeTest {
}
@Test
// Saves a login, then changes it and verifies the update
fun updateSavedLoginTest() {
val saveLoginTest =
TestAssetHelper.getSaveLoginAsset(mockWebServer)
@ -461,6 +571,7 @@ class SmokeTest {
}
@Test
// Verifies that you can go to System settings and change app's permissions from inside the app
fun redirectToAppPermissionsSystemSettingsTest() {
homeScreen {
}.openThreeDotMenu {
@ -490,4 +601,563 @@ class SmokeTest {
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.typeText
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.withId
import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.uiautomator.By
import androidx.test.uiautomator.By.text
import androidx.test.uiautomator.By.textContains
import androidx.test.uiautomator.UiScrollable
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.click
@ -31,7 +29,11 @@ import org.mozilla.fenix.helpers.ext.waitNotNull
*/
class AddToHomeScreenRobot {
fun verifyShortcutIcon() = assertShortcutIcon()
fun verifyAddPrivateBrowsingShortcutButton() = assertAddPrivateBrowsingShortcutButton()
fun verifyNoThanksPrivateBrowsingShortcutButton() = assertNoThanksPrivateBrowsingShortcutButton()
fun clickAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton().click()
fun addShortcutName(title: String) {
mDevice.waitNotNull(Until.findObject(By.text("Add to Home screen")), waitingTime)
@ -44,6 +46,8 @@ class AddToHomeScreenRobot {
fun clickAddShortcutButton() = addButton().click()
fun clickCancelShortcutButton() = cancelAddToHomeScreenButton().click()
fun clickAddAutomaticallyButton() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
mDevice.wait(Until.findObject(textContains("add automatically")), waitingTime)
@ -92,15 +96,19 @@ private fun assertShortcutNameField(expectedText: String) {
.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() =
mDevice.findObject(UiSelector().textContains("add automatically"))
private fun assertShortcutIcon() {
mDevice.wait(
Until.findObject(text("Firefox Preview")),
waitingTime
)
Assert.assertTrue(mDevice.hasObject(By.text("Firefox Preview")))
}
private fun addPrivateBrowsingShortcutButton() = onView(withId(R.id.cfr_pos_button))
private fun assertAddPrivateBrowsingShortcutButton() = addPrivateBrowsingShortcutButton()
.check(matches(ViewMatchers.withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
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.withParent
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.uiautomator.By
import androidx.test.uiautomator.By.res
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.Matchers.allOf
import org.hamcrest.Matchers.containsString
import org.junit.Assert.assertEquals
@ -52,7 +52,7 @@ class BookmarksRobot {
assertBookmarksView()
}
fun verifyEmptyBookmarksList() = assertEmptyBookmarksList()
fun verifyDeleteMultipleBookmarksSnackBar() = assertSnackBarText("Bookmarks deleted")
fun verifyBookmarkFavicon(forUrl: Uri) = assertBookmarkFavicon(forUrl)
@ -119,6 +119,13 @@ class BookmarksRobot {
fun verifyDeleteFolderConfirmationMessage() = assertDeleteFolderConfirmationMessage()
fun cancelFolderDeletion() {
onView(withText("CANCEL"))
.inRoot(RootMatchers.isDialog())
.check(matches(isDisplayed()))
.click()
}
fun createFolder(name: String) {
clickAddFolderButton()
addNewFolderName(name)
@ -133,8 +140,6 @@ class BookmarksRobot {
addFolderButton().click()
}
fun clickdeleteBookmarkButton() = deleteBookmarkButton().click()
fun addNewFolderName(name: String) {
addFolderTitleField()
.click()
@ -167,7 +172,7 @@ class BookmarksRobot {
fun saveEditBookmark() {
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()
@ -191,31 +196,6 @@ class BookmarksRobot {
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 {
mDevice.waitNotNull(Until.findObject(res("$packageName:id/overflow_menu")))
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))
@ -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 deleteBookmarkButton() = onView(withId(R.id.delete_bookmark_button))
private fun threeDotMenu(bookmarkUrl: Uri) = onView(
allOf(
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 snackBarUndoButton() = onView(withId(R.id.snackbar_btn))
@ -307,7 +283,7 @@ private fun assertBookmarksView() {
withParent(withId(R.id.navigationToolbar))
)
)
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE)))
.check(matches(isDisplayed()))
}
private fun assertEmptyBookmarksList() =
@ -322,7 +298,7 @@ private fun assertBookmarkFavicon(forUrl: Uri) = bookmarkFavicon(forUrl.toString
)
private fun assertBookmarkURL(expectedURL: String) =
mDevice.findObject(UiSelector().text(expectedURL))
bookmarkURL(expectedURL).check(matches(isDisplayed()))
private fun assertFolderTitle(expectedTitle: String) =
onView(withText(expectedTitle)).check(matches(isDisplayed()))
@ -360,13 +336,13 @@ private fun assertKeyboardVisibility(isExpectedToBeVisible: Boolean) =
)
private fun assertShareOverlay() =
onView(withId(R.id.shareWrapper)).check(matches(ViewMatchers.isDisplayed()))
onView(withId(R.id.shareWrapper)).check(matches(isDisplayed()))
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() =
onView(withId(R.id.share_tab_favicon)).check(matches(ViewMatchers.isDisplayed()))
onView(withId(R.id.share_tab_favicon)).check(matches(isDisplayed()))
private fun assertShareBookmarkUrl() =
onView(withId(R.id.share_tab_url)).check(matches(isDisplayed()))

@ -57,14 +57,14 @@ class BrowserRobot {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
sessionLoadedIdlingResource = SessionLoadedIdlingResource()
mDevice.waitNotNull(
Until.findObject(By.res("$packageName:id/mozac_browser_toolbar_url_view")),
waitingTime
)
runWithIdleRes(sessionLoadedIdlingResource) {
onView(withId(R.id.mozac_browser_toolbar_url_view))
.check(matches(withText(containsString(url.replace("http://", "")))))
assertTrue(
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 verifyNavURLBarHidden() = assertNavURLBarHidden()
fun verifySecureConnectionLockIcon() = assertSecureConnectionLockIcon()
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) {
onView(withText(containsURL.toString()))
.inRoot(isDialog())
@ -297,6 +306,8 @@ class BrowserRobot {
fun createBookmark(url: Uri) {
navigationToolbar {
}.enterURLAndEnterToBrowser(url) {
// needs to wait for the right url to load before saving a bookmark
verifyUrl(url.toString())
}.openThreeDotMenu {
clickAddBookmarkButton()
}
@ -445,6 +456,15 @@ class BrowserRobot {
NotificationRobot().interact()
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()
}
fun navURLBar() = onView(withId(R.id.mozac_browser_toolbar_url_view))
fun navURLBar() = onView(withId(R.id.toolbar))
private fun assertNavURLBar() = navURLBar()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNavURLBarHidden() = navURLBar()
.check(matches(not(isDisplayed())))
fun enhancedTrackingProtectionIndicator() =
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.withText
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withContentDescription
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestHelper
@ -41,6 +44,23 @@ class DownloadRobot {
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 {
fun clickDownload(interact: DownloadRobot.() -> Unit): Transition {
clickDownloadButton().click()
@ -85,6 +105,13 @@ class DownloadRobot {
DownloadRobot().interact()
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.action.ViewActions
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.swipeRight
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.Until
import androidx.test.uiautomator.Until.findObject
import mozilla.components.support.ktx.android.content.appName
import mozilla.components.browser.state.state.searchEngines
import mozilla.components.support.ktx.android.content.appName
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.instanceOf
@ -48,7 +47,6 @@ import org.hamcrest.Matchers
import org.junit.Assert
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.helpers.TestAssetHelper
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
import org.mozilla.fenix.helpers.TestHelper.packageName
import org.mozilla.fenix.helpers.TestHelper.scrollToElementByText
@ -74,7 +72,6 @@ class HomeScreenRobot {
fun verifyHomeMenu() = assertHomeMenu()
fun verifyTabButton() = assertTabButton()
fun verifyCollectionsHeader() = assertCollectionsHeader()
fun verifyNoCollectionsHeader() = assertNoCollectionsHeader()
fun verifyNoCollectionsText() = assertNoCollectionsText()
fun verifyHomeWordmark() = assertHomeWordmark()
fun verifyHomeToolbar() = assertHomeToolbar()
@ -152,25 +149,18 @@ class HomeScreenRobot {
fun confirmDeleteCollection() {
onView(allOf(withText("DELETE"))).click()
mDevice.waitNotNull(findObject(By.res("org.mozilla.fenix.debug:id/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")))
mDevice.waitNotNull(
findObject(By.res("org.mozilla.fenix.debug:id/no_collections_header")),
waitingTime
)
}
fun saveTabsSelectedForCollection() = onView(withId(R.id.save_button)).click()
fun verifyCollectionIsDisplayed(title: String) {
mDevice.wait(findObject(text(title)), waitingTime)
mDevice.findObject(UiSelector().text(title)).waitForExists(waitingTime)
collectionTitle(title).check(matches(isDisplayed()))
}
fun verifyCollectionIcon() =
onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
fun verifyCollectionIcon() = onView(withId(R.id.collection_icon)).check(matches(isDisplayed()))
fun expandCollection(title: String) {
try {
@ -190,9 +180,7 @@ class HomeScreenRobot {
}
}
fun clickSaveCollectionButton() = saveCollectionButton().click()
fun verifyItemInCollectionExists(title: String, visible: Boolean = true) {
fun verifyTabSavedInCollection(title: String, visible: Boolean = true) {
try {
collectionItem(title)
.check(
@ -203,11 +191,11 @@ class HomeScreenRobot {
}
}
fun verifyCollectionItemLogo() =
onView(withId(R.id.list_item_favicon)).check(matches(isDisplayed()))
fun verifyCollectionTabLogo() =
onView(withId(R.id.favicon)).check(matches(isDisplayed()))
fun verifyCollectionItemUrl() =
onView(withId(R.id.list_item_url)).check(matches(isDisplayed()))
fun verifyCollectionTabUrl() =
onView(withId(R.id.caption)).check(matches(isDisplayed()))
fun verifyShareCollectionButtonIsVisible(visible: Boolean) {
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 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 {
val appView = UiScrollable(UiSelector().scrollable(true))
appView.scrollTextIntoView(text)
return appView
}
fun closeTab() {
closeTabButton().click()
}
fun togglePrivateBrowsingModeOnOff() {
onView(ViewMatchers.withResourceName("privateBrowsingButton"))
.perform(click())
@ -337,7 +267,7 @@ class HomeScreenRobot {
fun verifySnackBarText(expectedText: String) {
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) {
@ -409,6 +339,22 @@ class HomeScreenRobot {
.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() {
onView(ViewMatchers.isRoot()).perform(ViewActions.pressBack())
}
@ -501,12 +447,11 @@ class HomeScreenRobot {
return BrowserRobot.Transition()
}
fun openTab(title: String, interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
mDevice.waitNotNull(findObject(text(title)))
tab(title).click()
fun clickSaveTabsToCollectionButton(interact: TabDrawerRobot.() -> Unit): TabDrawerRobot.Transition {
saveTabsToCollectionButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
TabDrawerRobot().interact()
return TabDrawerRobot.Transition()
}
}
}
@ -564,17 +509,14 @@ private fun assertCollectionsHeader() =
onView(allOf(withText("Collections")))
.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() =
onView(
allOf(
withText("Group together similar searches, sites, and tabs for quick access later.")
withText(
containsString("Collect the things that matter to you.\n" +
"Group together similar searches, sites, and tabs for quick access later."
)
)
)
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
).check(matches(isDisplayed()))
private fun assertHomeComponent() =
onView(ViewMatchers.withResourceName("sessionControlRecyclerView"))
@ -752,9 +694,6 @@ private fun assertPrivateSessionMessage() =
private fun collectionThreeDotButton() =
onView(allOf(withId(R.id.collection_overflow_button)))
private fun collectionNameTextField() =
onView(allOf(ViewMatchers.withResourceName("name_collection_edittext")))
private fun collectionTitle(title: String) =
onView(allOf(withId(R.id.collection_title), withText(title)))
@ -764,7 +703,7 @@ private fun assertExistingTopSitesList() =
private fun assertExistingTopSitesTabs(title: String) =
onView(allOf(withId(R.id.top_sites_list)))
.check(matches(hasItem(hasDescendant(withText(title)))))
.check(matches(hasDescendant(withText(title))))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun assertNotExistingTopSitesList(title: String) =
@ -794,21 +733,20 @@ private fun assertShareTabsOverlay() {
private fun tabMediaControlButton() = onView(withId(R.id.play_pause_button))
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 removeTabFromCollectionButton(title: String) =
onView(
allOf(
withId(R.id.list_item_action_button),
withId(R.id.secondary_button),
hasSibling(withText(title))
)
)
private fun collectionFlowBackButton() = onView(withId(R.id.back_button))
private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun tab(title: String) =

@ -11,21 +11,27 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingRegistry
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.ViewInteraction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.pressImeActionButton
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.action.ViewActions.typeText
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
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.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.platform.app.InstrumentationRegistry
import androidx.test.uiautomator.By
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.Until
import org.hamcrest.CoreMatchers.allOf
import org.hamcrest.CoreMatchers.anyOf
import org.hamcrest.CoreMatchers.containsString
import org.hamcrest.CoreMatchers.not
@ -53,8 +59,13 @@ class NavigationToolbarRobot {
fun verifyTabButtonShortcutMenuItems() = assertTabButtonShortcutMenuItems()
fun verifyReaderViewDetected(visible: Boolean = false): ViewInteraction =
assertReaderViewDetected(visible)
fun typeSearchTerm(searchTerm: String) = awesomeBar().perform(typeText(searchTerm))
fun toggleReaderView() = readerViewToggle().click()
class Transition {
private lateinit var sessionLoadedIdlingResource: SessionLoadedIdlingResource
@ -98,8 +109,9 @@ class NavigationToolbarRobot {
runWithIdleRes(sessionLoadedIdlingResource) {
onView(
anyOf(
ViewMatchers.withResourceName("browserLayout"),
ViewMatchers.withResourceName("onboarding_message") // Req ETP dialog
withResourceName("browserLayout"),
withResourceName("onboarding_message"), // Req ETP dialog
withResourceName("download_button")
)
)
.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 clearAddressBar() = onView(withId(R.id.mozac_browser_toolbar_clear_view))
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) {
try {
IdlingRegistry.getInstance().register(ir)

@ -17,16 +17,12 @@ import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.click
import org.mozilla.fenix.helpers.nthChildOf
/**
* Implementation of Robot Pattern for Reader View UI.
*/
class ReaderViewRobot {
fun verifyReaderViewDetected(visible: Boolean = false): ViewInteraction =
assertReaderViewDetected(visible)
fun verifyAppearanceFontGroup(visible: Boolean = false): ViewInteraction =
assertAppearanceFontGroup(visible)
@ -184,18 +180,6 @@ fun readerViewRobot(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Trans
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) =
onView(
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()
verifyUsageAndTechnicalDataSwitchDefault()
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 {
@ -80,8 +82,10 @@ private fun assertDataCollectionOptions() {
onView(withText(marketingDataText))
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
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)))
// Temporarily disabled until https://github.com/mozilla-mobile/fenix/issues/17086 and
// 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))

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

@ -17,6 +17,7 @@ import androidx.test.espresso.UiController
import androidx.test.espresso.ViewAction
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.replaceText
import androidx.test.espresso.assertion.ViewAssertions.doesNotExist
import androidx.test.espresso.assertion.ViewAssertions.matches
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.text
import androidx.test.uiautomator.UiDevice
import androidx.test.uiautomator.UiSelector
import androidx.test.uiautomator.Until
import androidx.test.uiautomator.Until.findObject
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.
*/
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 verifyCloseTabsButton(title: String) = assertCloseTabsButton(title)
@ -64,8 +79,12 @@ class TabDrawerRobot {
fun verifyTabTrayIsClosed() = assertTabTrayDoesNotExist()
fun verifyHalfExpandedRatio() = assertMinisculeHalfExpandedRatio()
fun verifyBehaviorState(expectedState: Int) = assertBehaviorState(expectedState)
fun verifyOpenedTabThumbnail() = assertTabThumbnail()
fun closeTab() {
mDevice.findObject(
UiSelector().resourceId("org.mozilla.fenix.debug:id/mozac_browser_tabstray_close")
).waitForExists(waitingTime)
closeTabButton().click()
}
@ -91,6 +110,9 @@ class TabDrawerRobot {
}
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(
matches(withEffectiveVisibility(ViewMatchers.Visibility.VISIBLE))
).perform(click())
@ -111,6 +133,32 @@ class TabDrawerRobot {
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 {
val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -123,7 +171,8 @@ class TabDrawerRobot {
}
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()
@ -226,6 +275,24 @@ class TabDrawerRobot {
}
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)))
}
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) =
onView(
allOf(
@ -323,3 +399,9 @@ private fun tabsCounter() = onView(withId(R.id.tab_button))
private fun visibleOrGone(visibility: Boolean) =
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 org.hamcrest.Matcher
import org.hamcrest.Matchers.allOf
import org.junit.Assert.assertTrue
import org.mozilla.fenix.R
import org.mozilla.fenix.helpers.TestAssetHelper.waitingTime
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.
*/
class ThreeDotMenuMainRobot {
fun verifyTabSettingsButton() = assertTabSettingsButton()
fun verifyRecentlyClosedTabsButton() = assertRecentlyClosedTabsButton()
fun verifyShareAllTabsButton() = assertShareAllTabsButton()
fun clickShareAllTabsButton() = shareAllTabsButton().click()
fun verifySettingsButton() = assertSettingsButton()
fun verifyAddOnsButton() = assertAddOnsButton()
fun verifyHistoryButton() = assertHistoryButton()
@ -60,8 +65,8 @@ class ThreeDotMenuMainRobot {
fun verifyRefreshButton() = assertRefreshButton()
fun verifyCloseAllTabsButton() = assertCloseAllTabsButton()
fun verifyShareButton() = assertShareButton()
fun verifyReaderViewToggle(visible: Boolean) = assertReaderViewToggle(visible)
fun verifyReaderViewAppearance(visible: Boolean) = assertReaderViewAppearanceButton(visible)
fun clickShareButton() {
shareButton().click()
mDevice.waitNotNull(Until.findObject(By.text("ALL ACTIONS")), waitingTime)
@ -71,7 +76,7 @@ class ThreeDotMenuMainRobot {
fun verifySaveCollection() = assertSaveCollectionButton()
fun verifySelectTabs() = assertSelectTabsButton()
fun clickBrowserViewSaveCollectionButton() {
fun clickSaveCollectionButton() {
browserViewSaveCollectionButton().click()
}
@ -116,6 +121,7 @@ class ThreeDotMenuMainRobot {
fun verifyAddToMobileHome() = assertAddToMobileHome()
fun verifyDesktopSite() = assertDesktopSite()
fun verifyDownloadsButton() = assertDownloadsButton()
fun verifyShareTabsOverlay() = assertShareTabsOverlay()
fun verifyThreeDotMainMenuItems() {
verifyAddOnsButton()
@ -135,6 +141,13 @@ class ThreeDotMenuMainRobot {
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 {
private val mDevice = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
@ -150,6 +163,14 @@ class ThreeDotMenuMainRobot {
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 {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Synced tabs")), waitingTime)
@ -160,9 +181,11 @@ class ThreeDotMenuMainRobot {
}
fun openBookmarks(interact: BookmarksRobot.() -> Unit): BookmarksRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.findObject(UiSelector().resourceId("R.id.bookmark_list")).waitForExists(waitingTime)
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Bookmarks")), waitingTime)
bookmarksButton().click()
assertTrue(mDevice.findObject(UiSelector().resourceId("org.mozilla.fenix.debug:id/bookmark_list")).waitForExists(waitingTime))
BookmarksRobot().interact()
return BookmarksRobot.Transition()
@ -251,6 +274,13 @@ class ThreeDotMenuMainRobot {
return HomeScreenRobot.Transition()
}
fun openReportSiteIssue(interact: BrowserRobot.() -> Unit): BrowserRobot.Transition {
reportSiteIssueButton().click()
BrowserRobot().interact()
return BrowserRobot.Transition()
}
fun openFindInPage(interact: FindInPageRobot.() -> Unit): FindInPageRobot.Transition {
onView(withId(R.id.mozac_browser_menu_recyclerView)).perform(ViewActions.swipeDown())
mDevice.waitNotNull(Until.findObject(By.text("Find in page")), waitingTime)
@ -288,13 +318,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun toggleReaderView(interact: NavigationToolbarRobot.() -> Unit): NavigationToolbarRobot.Transition {
readerViewToggle().click()
NavigationToolbarRobot().interact()
return NavigationToolbarRobot.Transition()
}
fun openReaderViewAppearance(interact: ReaderViewRobot.() -> Unit): ReaderViewRobot.Transition {
readerViewAppearanceToggle().click()
@ -317,6 +340,13 @@ class ThreeDotMenuMainRobot {
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 {
mDevice.waitNotNull(Until.findObject(By.text(title)), waitingTime)
onView(withText(title)).click()
@ -325,13 +355,6 @@ class ThreeDotMenuMainRobot {
return BrowserRobot.Transition()
}
fun clickOpenTabsMenuSaveCollection(interact: HomeScreenRobot.() -> Unit): HomeScreenRobot.Transition {
saveCollectionButton().click()
HomeScreenRobot().interact()
return HomeScreenRobot.Transition()
}
fun openSaveToCollection(interact: ThreeDotMenuMainRobot.() -> Unit): ThreeDotMenuMainRobot.Transition {
mDevice.waitNotNull(Until.findObject(By.text("Save to collection")), waitingTime)
saveCollectionButton().click()
@ -444,6 +467,8 @@ private fun collectionNameTextField() =
private fun assertCollectionNameTextField() = collectionNameTextField()
.check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
private fun reportSiteIssueButton() = onView(withText("Report Site Issue…"))
private fun findInPageButton() = onView(allOf(withText("Find in page")))
private fun assertFindInPageButton() = findInPageButton()
@ -474,12 +499,6 @@ private fun assertWhatsNewButton() = whatsNewButton()
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() =
onView(allOf(withText(R.string.browser_menu_read_appearance)))
@ -511,6 +530,8 @@ private fun assertAddToMobileHome() {
).check(matches(withEffectiveVisibility(Visibility.VISIBLE)))
}
private fun installPWAButton() = onView(allOf(withId(R.id.highlight_text), withText("Install")))
private fun desktopSiteButton() =
onView(allOf(withText(R.string.browser_menu_desktop_site)))
private fun assertDesktopSite() {
@ -530,3 +551,30 @@ private fun clickAddonsManagerButton() {
}
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 {
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 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)"
}
}

@ -21,29 +21,27 @@ object FeatureFlags {
*/
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
* 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.
*/
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.components.browser.session.Session
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.concept.base.crash.Breadcrumb
import mozilla.components.concept.push.PushProcessor
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.metrics.MetricServiceType
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.perf.StorageStatsMetrics
import org.mozilla.fenix.perf.runBlockingIncrement
import org.mozilla.fenix.push.PushFxaIntegration
import org.mozilla.fenix.push.WebPushEngineIntegration
import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks
import org.mozilla.fenix.session.VisibilityLifecycleCallback
import org.mozilla.fenix.utils.BrowsersCache
import java.util.concurrent.TimeUnit
/**
*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()
}
initializeWebExtensionSupport()
restoreBrowserState()
restoreDownloads()
// Just to make sure it is impossible for any application-services pieces
@ -159,6 +161,20 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
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() {
components.useCases.downloadUseCases.restoreDownloads()
}
@ -398,7 +414,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
onNewTabOverride = {
_, engineSession, url ->
val shouldCreatePrivateSession =
components.core.sessionManager.selectedSession?.private
components.core.store.state.selectedTab?.content?.private
?: components.settings.openLinksInAPrivateTab
val session = Session(url, shouldCreatePrivateSession)
@ -409,9 +425,7 @@ open class FenixApplication : LocaleAwareApplication(), Provider {
_, sessionId -> components.useCases.tabsUseCases.removeTab(sessionId)
},
onSelectTabOverride = {
_, sessionId ->
val selected = components.core.sessionManager.findSessionById(sessionId)
selected?.let { components.useCases.tabsUseCases.selectTab(it) }
_, sessionId -> components.useCases.tabsUseCases.selectTab(sessionId)
},
onExtensionsLoaded = { extensions ->
components.addonUpdater.registerForFutureUpdates(extensions)

@ -14,6 +14,7 @@ import android.os.SystemClock
import android.text.format.DateUtils
import android.util.AttributeSet
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewConfiguration
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.recentlyclosed.RecentlyClosedFragmentDirections
import org.mozilla.fenix.perf.Performance
import org.mozilla.fenix.perf.PerformanceInflater
import org.mozilla.fenix.perf.StartupTimeline
import org.mozilla.fenix.search.SearchDialogFragmentDirections
import org.mozilla.fenix.session.PrivateNotificationService
@ -138,6 +140,8 @@ open class HomeActivity : LocaleAwareAppCompatActivity(), NavHostActivity {
WebExtensionPopupFeature(components.core.store, ::openPopup)
}
private var inflater: LayoutInflater? = null
private val navHost by lazy {
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 {
return DefaultBrowsingModeManager(initialMode, components.settings) { 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.settings
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
import org.mozilla.fenix.theme.ThemeManager
import java.util.Locale
import java.lang.ref.WeakReference

@ -5,7 +5,6 @@
package org.mozilla.fenix.addons
import android.view.View
import androidx.fragment.app.Fragment
import org.mozilla.fenix.components.FenixSnackbar
/**
@ -23,14 +22,3 @@ internal fun showSnackBar(view: View, text: String, duration: Int = FenixSnackba
.setText(text)
.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.ext.components
import org.mozilla.fenix.ext.showToolbar
import org.mozilla.fenix.ext.runIfFragmentIsAttached
/**
* 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.view.*
import kotlinx.coroutines.ExperimentalCoroutinesApi
import mozilla.components.browser.session.Session
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.toolbar.BrowserToolbar
import mozilla.components.feature.app.links.AppLinksUseCases
@ -47,78 +48,110 @@ import org.mozilla.fenix.trackingprotection.TrackingProtectionOverlay
class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
private val windowFeature = ViewBoundFeatureWrapper<WindowFeature>()
private val openInAppOnboardingObserver = ViewBoundFeatureWrapper<OpenInAppOnboardingObserver>()
private val trackingProtectionOverlayObserver = ViewBoundFeatureWrapper<TrackingProtectionOverlay>()
private var readerModeAvailable = false
private var openInAppOnboardingObserver: OpenInAppOnboardingObserver? = null
private var pwaOnboardingObserver: PwaOnboardingObserver? = null
@Suppress("LongMethod")
override fun initializeUI(view: View): Session? {
override fun initializeUI(view: View, tab: SessionState) {
super.initializeUI(view, tab)
val context = requireContext()
val components = context.components
return super.initializeUI(view)?.also {
if (context.settings().isSwipeToolbarToSwitchTabsEnabled) {
gestureLayout.addGestureListener(
ToolbarGestureHandler(
activity = requireActivity(),
contentLayout = browserLayout,
tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view,
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
if (context.settings().isSwipeToolbarToSwitchTabsEnabled) {
gestureLayout.addGestureListener(
ToolbarGestureHandler(
activity = requireActivity(),
contentLayout = browserLayout,
tabPreview = tabPreview,
toolbarLayout = browserToolbarView.view,
store = components.core.store,
sessionManager = components.core.sessionManager
)
)
}
browserToolbarView.view.addPageAction(readerModeAction)
thumbnailsFeature.set(
feature = BrowserThumbnails(context, view.engineView, components.core.store),
owner = this,
view = view
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
)
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)
}
browserToolbarView.view.addPageAction(readerModeAction)
readerModeAvailable = available
readerModeAction.setSelected(active)
safeInvalidateBrowserToolbarView()
thumbnailsFeature.set(
feature = BrowserThumbnails(context, view.engineView, components.core.store),
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,
view = view
)
windowFeature.set(
feature = WindowFeature(
store = components.core.store,
tabsUseCases = components.useCases.tabsUseCases
}
if (context.settings().shouldShowTrackingProtectionCfr) {
trackingProtectionOverlayObserver.set(
feature = TrackingProtectionOverlay(
context = context,
store = context.components.core.store,
lifecycleOwner = viewLifecycleOwner,
settings = context.settings(),
metrics = context.components.analytics.metrics,
getToolbar = { browserToolbarView.view }
),
owner = this,
view = view
@ -130,36 +163,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
super.onStart()
val context = requireContext()
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) {
pwaOnboardingObserver = PwaOnboardingObserver(
@ -178,14 +181,6 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
override fun 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()
}
@ -208,29 +203,30 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
return readerViewFeature.onBackPressed() || super.onBackPressed()
}
override fun navToQuickSettingsSheet(session: Session, sitePermissions: SitePermissions?) {
override fun navToQuickSettingsSheet(tab: SessionState, sitePermissions: SitePermissions?) {
val directions =
BrowserFragmentDirections.actionBrowserFragmentToQuickSettingsSheetDialogFragment(
sessionId = session.id,
url = session.url,
title = session.title,
isSecured = session.securityInfo.secure,
sessionId = tab.id,
url = tab.content.url,
title = tab.content.title,
isSecured = tab.content.securityInfo.secure,
sitePermissions = sitePermissions,
gravity = getAppropriateLayoutGravity(),
certificateName = session.securityInfo.issuer
certificateName = tab.content.securityInfo.issuer,
permissionHighlights = tab.content.permissionHighlights
)
nav(R.id.browserFragment, directions)
}
override fun navToTrackingProtectionPanel(session: Session) {
override fun navToTrackingProtectionPanel(tab: SessionState) {
val navController = findNavController()
requireComponents.useCases.trackingProtectionUseCases.containsException(session.id) { contains ->
val isEnabled = session.trackerBlockingEnabled && !contains
requireComponents.useCases.trackingProtectionUseCases.containsException(tab.id) { contains ->
val isEnabled = tab.trackingProtection.enabled && !contains
val directions =
BrowserFragmentDirections.actionBrowserFragmentToTrackingProtectionPanelDialogFragment(
sessionId = session.id,
url = session.url,
sessionId = tab.id,
url = tab.content.url,
trackingProtectionEnabled = isEnabled,
gravity = getAppropriateLayoutGravity()
)
@ -239,11 +235,11 @@ class BrowserFragment : BaseBrowserFragment(), UserInteractionHandler {
}
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)
}
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<Session>) {
override fun onTabsAdded(tabCollection: TabCollection, sessions: List<TabSessionState>) {
showTabSavedToCollectionSnackbar(sessions.size)
}

@ -7,10 +7,20 @@ package org.mozilla.fenix.browser
import android.content.Context
import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.lifecycle.LifecycleOwner
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.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlin.tryGetHostFromUrl
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifAnyChanged
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav
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.
*/
@ExperimentalCoroutinesApi
@Suppress("LongParameterList")
class OpenInAppOnboardingObserver(
private val context: Context,
private val store: BrowserStore,
private val lifecycleOwner: LifecycleOwner,
private val navController: NavController,
private val settings: Settings,
private val appLinksUseCases: AppLinksUseCases,
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
internal var infoBanner: InfoBanner? = null
override fun onUrlChanged(session: Session, url: String) {
sessionDomainForDisplayedBanner?.let {
if (url.tryGetHostFromUrl() != it) {
infoBanner?.dismiss()
override fun start() {
scope = store.flowScoped(lifecycleOwner) { flow ->
flow.mapNotNull { state ->
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) {
return
}
val appLink = appLinksUseCases.appLinkRedirect
if (appLink(session.url).hasExternalApp()) {
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)
}
if (appLink(url).hasExternalApp()) {
infoBanner = createInfoBanner()
infoBanner?.showBanner()
sessionDomainForDisplayedBanner = session.url.tryGetHostFromUrl()
sessionDomainForDisplayedBanner = url.tryGetHostFromUrl()
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 mozilla.components.browser.session.Session
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 org.mozilla.fenix.R
import org.mozilla.fenix.ext.getRectWithScreenLocation
@ -40,6 +42,7 @@ class ToolbarGestureHandler(
private val contentLayout: View,
private val tabPreview: TabPreview,
private val toolbarLayout: View,
private val store: BrowserStore,
private val sessionManager: SessionManager
) : SwipeGestureListener {
@ -145,15 +148,15 @@ class ToolbarGestureHandler(
private fun getDestination(): Destination {
val isLtr = activity.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR
val currentSession = sessionManager.selectedSession ?: return Destination.None
val currentIndex = sessionManager.sessionsOfType(currentSession.private).indexOfFirst {
it.id == currentSession.id
val currentTab = store.state.selectedTab ?: return Destination.None
val currentIndex = sessionManager.sessionsOfType(currentTab.content.private).indexOfFirst {
it.id == currentTab.id
}
return if (currentIndex == -1) {
Destination.None
} else {
val sessions = sessionManager.sessionsOfType(currentSession.private)
val sessions = sessionManager.sessionsOfType(currentTab.content.private)
val index = when (gestureDirection) {
GestureDirection.RIGHT_TO_LEFT -> if (isLtr) {
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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.selector.findTab
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 org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.ext.getDefaultCollectionNumber
import org.mozilla.fenix.ext.normalSessionSize
import org.mozilla.fenix.home.Tab
interface CollectionCreationController {
@ -59,24 +60,24 @@ interface CollectionCreationController {
fun removeTabFromSelection(tab: Tab)
}
fun List<Tab>.toSessionBundle(sessionManager: SessionManager): List<Session> {
return this.mapNotNull { sessionManager.findSessionById(it.sessionId) }
fun List<Tab>.toTabSessionStateList(store: BrowserStore): List<TabSessionState> {
return this.mapNotNull { store.state.findTab(it.sessionId) }
}
/**
* @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 metrics Controller that handles telemetry events.
* @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.
*/
class DefaultCollectionCreationController(
private val store: CollectionCreationStore,
private val browserStore: BrowserStore,
private val dismiss: () -> Unit,
private val metrics: MetricController,
private val tabCollectionStorage: TabCollectionStorage,
private val sessionManager: SessionManager,
private val scope: CoroutineScope
) : CollectionCreationController {
@ -88,13 +89,13 @@ class DefaultCollectionCreationController(
override fun saveCollectionName(tabs: List<Tab>, name: String) {
dismiss()
val sessionBundle = tabs.toSessionBundle(sessionManager)
val sessionBundle = tabs.toTabSessionStateList(browserStore)
scope.launch {
tabCollectionStorage.createCollection(name, sessionBundle)
}
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>) {
dismiss()
val sessionBundle = tabs.toList().toSessionBundle(sessionManager)
val sessionBundle = tabs.toList().toTabSessionStateList(browserStore)
scope.launch {
tabCollectionStorage
.addTabsToCollection(collection, sessionBundle)
}
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(
DefaultCollectionCreationController(
collectionCreationStore,
requireComponents.core.store,
::dismiss,
requireComponents.analytics.metrics,
requireComponents.core.tabCollectionStorage,
requireComponents.core.sessionManager,
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.update.AddonUpdater
import mozilla.components.feature.addons.update.DefaultAddonUpdater
import mozilla.components.feature.sitepermissions.SitePermissionsStorage
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
import mozilla.components.support.migration.state.MigrationStore
import io.github.forkmaintainers.iceraven.components.PagedAddonCollectionProvider
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.components.metrics.AppStartupTelemetry
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.utils.ClipboardHandler
@ -28,7 +31,7 @@ import org.mozilla.fenix.utils.Settings
import org.mozilla.fenix.wifi.WifiConnectionMonitor
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
@ -70,6 +73,7 @@ class Components(private val context: Context) {
core.store,
useCases.sessionUseCases,
useCases.tabsUseCases,
useCases.customTabsUseCases,
useCases.searchUseCases,
core.relationChecker,
core.customTabsStore,
@ -93,9 +97,10 @@ class Components(private val context: Context) {
PagedAddonCollectionProvider(
context,
core.client,
serverURL = BuildConfig.AMO_SERVER_URL,
collectionAccount = context.settings().customAddonsAccount,
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 {
action = Intent.ACTION_VIEW
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)
}
val sitePermissionsStorage by lazyMonitored {
SitePermissionsStorage(context, context.components.core.engine)
}
val analytics by lazyMonitored { Analytics(context) }
val publicSuffixList by lazyMonitored { PublicSuffixList(context) }
val clipboardHandler by lazyMonitored { ClipboardHandler(context) }

@ -11,10 +11,6 @@ import android.os.Build
import android.os.StrictMode
import androidx.core.content.ContextCompat
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.fetch.GeckoViewFetchClient
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.storage.SessionStorage
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.store.BrowserStore
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.downloads.DownloadMiddleware
import mozilla.components.feature.logins.exceptions.LoginExceptionStorage
import mozilla.components.feature.media.MediaSessionFeature
import mozilla.components.feature.media.middleware.MediaMiddleware
import mozilla.components.feature.media.middleware.RecordingDevicesMiddleware
import mozilla.components.feature.pwa.ManifestStorage
import mozilla.components.feature.pwa.WebAppShortcutManager
@ -64,14 +61,17 @@ import mozilla.components.support.locale.LocaleManager
import org.mozilla.fenix.AppRequestInterceptor
import org.mozilla.fenix.BuildConfig
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags.newMediaSessionApi
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.TelemetryMiddleware
import org.mozilla.fenix.components.search.SearchMigration
import org.mozilla.fenix.downloads.DownloadService
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
import org.mozilla.fenix.media.MediaService
import org.mozilla.fenix.media.MediaSessionService
import org.mozilla.fenix.perf.StrictModeManager
import org.mozilla.fenix.perf.lazyMonitored
import org.mozilla.fenix.search.telemetry.ads.AdsTelemetry
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.utils.Mockable
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.
@ -166,7 +160,7 @@ class Core(
)
}
private val sessionStorage: SessionStorage by lazyMonitored {
val sessionStorage: SessionStorage by lazyMonitored {
SessionStorage(context, engine = engine)
}
@ -239,7 +233,7 @@ class Core(
* case all sessions/tabs are closed.
*/
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.
icons.install(engine, store)
@ -249,40 +243,6 @@ class Core(
// Install the "cookies" WebExtension and tracks user interaction with SERPs.
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(
context, engine, icons, R.drawable.ic_status_logo,
permissionStorage.permissionsStorage, HomeActivity::class.java
@ -346,7 +306,6 @@ class Core(
val tabCollectionStorage by lazyMonitored {
TabCollectionStorage(
context,
sessionManager,
strictMode
)
}

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

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

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

@ -7,8 +7,6 @@ package org.mozilla.fenix.components
import androidx.annotation.VisibleForTesting
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicyForSessionTypes
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.utils.Settings
/**
@ -49,7 +47,7 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
return TrackingProtectionPolicy.select(
cookiePolicy = getCustomCookiePolicy(),
trackingCategories = getCustomTrackingCategories(),
cookiePurging = Config.channel.isNightlyOrDebug
cookiePurging = getCustomCookiePurgingPolicy()
).let {
if (settings.blockTrackingContentSelectionInCustomTrackingProtection == "private") {
it.forPrivateSessionsOnly()
@ -95,6 +93,10 @@ class TrackingProtectionPolicyFactory(private val settings: Settings) {
return categories.toTypedArray()
}
private fun getCustomCookiePurgingPolicy(): Boolean {
return settings.blockRedirectTrackersInCustomTrackingProtection
}
}
@VisibleForTesting
@ -103,6 +105,6 @@ internal fun TrackingProtectionPolicyForSessionTypes.adaptPolicyToChannel(): Tra
trackingCategories = trackingCategories,
cookiePolicy = cookiePolicy,
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.SettingsUseCases
import mozilla.components.feature.session.TrackingProtectionUseCases
import mozilla.components.feature.tabs.CustomTabsUseCases
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSitesStorage
import mozilla.components.feature.top.sites.TopSitesUseCases
@ -48,6 +49,13 @@ class UseCases(
*/
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.
*/

@ -122,6 +122,7 @@ sealed class Event {
object NotificationMediaPlay : Event()
object NotificationMediaPause : Event()
object TopSiteOpenDefault : Event()
object TopSiteOpenGoogle : Event()
object TopSiteOpenFrecent : Event()
object TopSiteOpenPinned : Event()
object TopSiteOpenInNewTab : Event()
@ -168,6 +169,7 @@ sealed class Event {
object ContextualHintETPOutsideTap : Event()
object ContextualHintETPInsideTap : Event()
// Tab tray
object TabsTrayOpened : Event()
object TabsTrayClosed : Event()
object OpenedExistingTab : Event()
@ -180,6 +182,8 @@ sealed class Event {
object TabsTraySaveToCollectionPressed : Event()
object TabsTrayShareAllTabsPressed : Event()
object TabsTrayCloseAllTabsPressed : Event()
object TabsTrayCfrDismissed : Event()
object TabsTrayCfrTapped : Event()
object ProgressiveWebAppOpenFromHomescreenTap : Event()
object ProgressiveWebAppInstallAsShortcut : Event()
@ -195,6 +199,11 @@ sealed class Event {
object RecentlyClosedTabsOpened : Event()
object ContextMenuCopyTapped : Event()
object ContextMenuSearchTapped : Event()
object ContextMenuSelectAllTapped : Event()
object ContextMenuShareTapped : Event()
// Interaction events with extras
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 Widget(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)
private val label: String
@ -446,6 +456,7 @@ sealed class Event {
is Action -> "action"
is Widget -> "widget"
is Shortcut -> "shortcut"
is TopSite -> "topsite"
is Other -> "other"
}
@ -457,7 +468,7 @@ sealed class Event {
}
enum class SearchAccessPoint {
SUGGESTION, ACTION, WIDGET, SHORTCUT, NONE
SUGGESTION, ACTION, WIDGET, SHORTCUT, TOPSITE, NONE
}
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.ContextMenu
import org.mozilla.fenix.GleanMetrics.ContextualHintTrackingProtection
import org.mozilla.fenix.GleanMetrics.ContextualMenu
import org.mozilla.fenix.GleanMetrics.CrashReporter
import org.mozilla.fenix.GleanMetrics.CustomTab
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.Tabs
import org.mozilla.fenix.GleanMetrics.TabsTray
import org.mozilla.fenix.GleanMetrics.TabsTrayCfr
import org.mozilla.fenix.GleanMetrics.Tip
import org.mozilla.fenix.GleanMetrics.ToolbarSettings
import org.mozilla.fenix.GleanMetrics.TopSites
@ -513,6 +515,9 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TopSiteOpenDefault -> EventWrapper<NoExtraKeys>(
{ TopSites.openDefault.record(it) }
)
is Event.TopSiteOpenGoogle -> EventWrapper<NoExtraKeys>(
{ TopSites.openGoogleSearchAttribution.record(it) }
)
is Event.TopSiteOpenFrecent -> EventWrapper<NoExtraKeys>(
{ TopSites.openFrecency.record(it) }
)
@ -658,7 +663,13 @@ private val Event.wrapper: EventWrapper<*>?
is Event.TabsTrayCloseAllTabsPressed -> EventWrapper<NoExtraKeys>(
{ 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) }
)
is Event.AutoPlaySettingChanged -> EventWrapper(
@ -691,15 +702,27 @@ private val Event.wrapper: EventWrapper<*>?
{ Events.recentlyClosedTabsOpened.record(it) }
)
Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>(
is Event.MasterPasswordMigrationDisplayed -> EventWrapper<NoExtraKeys>(
{ MasterPassword.displayed.record(it) }
)
Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>(
is Event.MasterPasswordMigrationSuccess -> EventWrapper<NoExtraKeys>(
{ MasterPassword.migration.record(it) }
)
Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>(
is Event.TabSettingsOpened -> EventWrapper<NoExtraKeys>(
{ 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:
is Event.AddBookmark -> null

@ -153,6 +153,15 @@ internal class ReleaseMetricController(
Component.FEATURE_CONTEXTMENU to ContextMenuFacts.Items.ITEM -> {
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 -> {
metadata?.get("customTab")?.let { Event.CustomTabsMenuOpened }
@ -235,4 +244,15 @@ internal class ReleaseMetricController(
}
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.support.base.log.logger.Logger
import org.mozilla.fenix.components.metrics.Event.PerformedSearch.SearchAccessPoint
import org.mozilla.fenix.ext.components
import java.io.IOException
import java.security.NoSuchAlgorithmException
import java.security.spec.InvalidKeySpecException
@ -58,6 +57,11 @@ object MetricsUtils {
engineSource
)
)
SearchAccessPoint.TOPSITE -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.TopSite(
engineSource
)
)
SearchAccessPoint.NONE -> Event.PerformedSearch(
Event.PerformedSearch.EventSource.Other(
engineSource

@ -77,7 +77,7 @@ class DefaultBrowserToolbarController(
store.updateSearchTermsOfSelectedSession(text)
activity.components.useCases.searchUseCases.defaultSearch.invoke(
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 mozilla.components.browser.domains.autocomplete.ShippedDomainsProvider
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.selector.selectedTab
import mozilla.components.browser.state.state.ExternalAppType
import mozilla.components.browser.toolbar.BrowserToolbar
import mozilla.components.browser.toolbar.behavior.BrowserToolbarBottomBehavior
@ -98,7 +99,6 @@ class BrowserToolbarView(
}
with(container.context) {
val sessionManager = components.core.sessionManager
val isPinningSupported = components.useCases.webAppUseCases.isPinningSupported()
if (toolbarPosition == ToolbarPosition.TOP) {
@ -157,7 +157,10 @@ class BrowserToolbarView(
hint = secondaryTextColor,
separator = separatorColor,
trackingProtection = primaryTextColor,
permissionHighlights = primaryTextColor
highlight = ContextCompat.getColor(
context,
R.color.whats_new_notification_color
)
)
display.hint = context.getString(R.string.search_hint)
@ -211,7 +214,7 @@ class BrowserToolbarView(
components.core.historyStorage,
lifecycleOwner,
sessionId = null,
isPrivate = sessionManager.selectedSession?.private ?: false,
isPrivate = components.core.store.state.selectedTab?.content?.private ?: false,
interactor = interactor,
engine = components.core.engine
)

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

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

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

@ -7,6 +7,7 @@ package org.mozilla.fenix.customtabs
import android.content.Context
import android.graphics.Typeface
import androidx.annotation.ColorRes
import androidx.annotation.VisibleForTesting
import androidx.core.content.ContextCompat.getColor
import mozilla.components.browser.menu.BrowserMenuBuilder
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.BrowserMenuItemToolbar
import mozilla.components.browser.menu.item.SimpleBrowserMenuItem
import mozilla.components.browser.state.selector.findTab
import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.selector.findCustomTab
import mozilla.components.browser.state.state.CustomTabSessionState
import mozilla.components.browser.state.store.BrowserStore
import org.mozilla.fenix.R
import org.mozilla.fenix.components.toolbar.ToolbarMenu
@ -46,7 +47,8 @@ class CustomTabToolbarMenu(
override val menuBuilder by lazy { BrowserMenuBuilder(menuItems) }
/** 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)
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.display.DisplayToolbar
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.UserInteractionHandler
import org.mozilla.fenix.R
@ -20,6 +21,7 @@ import org.mozilla.fenix.ext.settings
class CustomTabsIntegration(
sessionManager: SessionManager,
store: BrowserStore,
useCases: CustomTabsUseCases,
toolbar: BrowserToolbar,
sessionId: String,
activity: Activity,
@ -84,9 +86,10 @@ class CustomTabsIntegration(
}
private val feature = CustomTabsToolbarFeature(
sessionManager,
store,
toolbar,
sessionId,
useCases,
menuBuilder = customTabToolbarMenu.menuBuilder,
menuItemIndex = START_OF_MENU_ITEMS_INDEX,
window = activity.window,

@ -9,7 +9,6 @@ import androidx.annotation.VisibleForTesting
import androidx.navigation.NavDestination
import androidx.navigation.NavDirections
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.state.SessionState
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
// 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.
components.core.sessionManager.runWithSession(getExternalTabId()) { session ->
// If the custom tag config has been removed we are opening this in normal browsing
if (session.customTabConfig != null) {
remove(session)
}
true
val tabId = getExternalTabId()
val customTab = tabId?.let { components.core.store.state.findCustomTab(it) }
if (tabId != null && customTab != null) {
components.useCases.customTabsUseCases.remove(tabId)
}
}
}

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

@ -106,7 +106,7 @@ class FennecWebAppIntentProcessor(
if (path.isNullOrEmpty()) return null
val file = File(path)
if (!file.isUnderFennecManifestDirectory()) return null
if (!isUnderFennecManifestDirectory(file)) return null
return try {
// 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/
*/
private fun File.isUnderFennecManifestDirectory(): Boolean {
val manifestsDir = canonicalFile.parentFile
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun isUnderFennecManifestDirectory(file: File): Boolean {
val manifestsDir = file.canonicalFile.parentFile
// 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"
manifestsDir.parentFile?.parentFile != getMozillaDirectory()
manifestsDir.parentFile?.parentFile?.canonicalPath == getMozillaDirectory().canonicalPath
}
private fun createFallbackCustomTabConfig(): CustomTabConfig {

@ -58,7 +58,7 @@ class PoweredByNotification(
val appName = getString(R.string.app_name)
return NotificationCompat.Builder(this, channelId)
.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)
.setColor(ContextCompat.getColor(this, R.color.primary_text_light_theme))
.setPriority(NotificationCompat.PRIORITY_MIN)

@ -6,9 +6,11 @@ package org.mozilla.fenix.downloads
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.downloads.AbstractFetchDownloadService
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
class DownloadService : AbstractFetchDownloadService() {
override val httpClient by lazy { components.core.client }
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()
}
/**
* 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.
* 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.LinearLayout
import android.widget.PopupWindow
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
@ -63,13 +64,13 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import mozilla.appservices.places.BookmarkRoot
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.normalTabs
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.state.BrowserState
import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.storage.FrecencyThresholdOption
import mozilla.components.concept.sync.AccountObserver
import mozilla.components.concept.sync.AuthType
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.requireComponents
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.SessionControlInteractor
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
get() = requireComponents.core.store
@ -234,10 +234,11 @@ class HomeFragment : Fragment() {
engine = components.core.engine,
metrics = components.analytics.metrics,
store = store,
sessionManager = sessionManager,
tabCollectionStorage = components.core.tabCollectionStorage,
addTabUseCase = components.useCases.tabsUseCases.addTab,
restoreUseCase = components.useCases.tabsUseCases.restore,
reloadUrlUseCase = components.useCases.sessionUseCases.reload,
selectTabUseCase = components.useCases.tabsUseCases.selectTab,
fragmentStore = homeFragmentStore,
navController = findNavController(),
viewLifecycleScope = viewLifecycleOwner.lifecycleScope,
@ -273,9 +274,13 @@ class HomeFragment : Fragment() {
* Returns a [TopSitesConfig] which specifies how many top sites to display and whether or
* not frequently visited sites should be displayed.
*/
private fun getTopSitesConfig(): TopSitesConfig {
@VisibleForTesting
internal fun getTopSitesConfig(): TopSitesConfig {
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) {
val iconSize =
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)
search_engine_icon?.setImageDrawable(searchIcon)
} else {
@ -471,9 +477,9 @@ class HomeFragment : Fragment() {
private fun removeAllTabsAndShowSnackbar(sessionCode: String) {
if (sessionCode == ALL_PRIVATE_TABS) {
sessionManager.removePrivateSessions()
requireComponents.useCases.tabsUseCases.removePrivateTabs()
} else {
sessionManager.removeNormalSessions()
requireComponents.useCases.tabsUseCases.removeNormalTabs()
}
val snackbarMessage = if (sessionCode == ALL_PRIVATE_TABS) {
@ -587,7 +593,9 @@ class HomeFragment : Fragment() {
}
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()
}
@ -693,10 +701,13 @@ class HomeFragment : Fragment() {
// We want to show the popup only after privateBrowsingButton is available.
// Otherwise, we will encounter an activity token error.
privateBrowsingButton.post {
context.settings().lastCfrShownTimeInMillis = System.currentTimeMillis()
privateBrowsingRecommend.showAsDropDown(
privateBrowsingButton, 0, CFR_Y_OFFSET, Gravity.TOP or Gravity.END
)
runIfFragmentIsAttached {
context.settings().showedPrivateModeContextualFeatureRecommender = true
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
private fun updateTabCounter(browserState: BrowserState) {
val tabCount = if (browsingModeManager.mode.isPrivate) {
view?.tab_button?.setColor(ContextCompat.getColor(requireContext(), R.color.primary_text_private_theme))
browserState.privateTabs.size
} else {
browserState.normalTabs.size
@ -1022,7 +1032,6 @@ class HomeFragment : Fragment() {
private const val FOCUS_ON_ADDRESS_BAR = "focusOnAddressBar"
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

@ -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 androidx.navigation.NavController
import mozilla.components.browser.state.selector.findTab
import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService
import org.mozilla.fenix.BrowserDirection
@ -25,11 +26,14 @@ class OpenSpecificTabIntentProcessor(
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
if (intent.action == getAction()) {
val sessionManager = activity.components.core.sessionManager
val sessionId = intent.extras?.getString(getTabId())
val session = sessionId?.let { sessionManager.findSessionById(it) }
val browserStore = activity.components.core.store
val tabId = intent.extras?.getString(getTabId())
// 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) {
sessionManager.select(session)
activity.components.useCases.tabsUseCases.selectTab(tabId)
activity.openToBrowser(BrowserDirection.FromGlobal)
return true
}

@ -43,7 +43,12 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
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 {
val newTopSites = (other as? TopSitePager) ?: return false
return newTopSites.topSites.size == this.topSites.size
@ -56,6 +61,19 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) {
val oldTopSites = this.topSites.asSequence()
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)
@ -195,6 +213,25 @@ class SessionControlAdapter(
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")
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)

@ -6,19 +6,21 @@ package org.mozilla.fenix.home.sessioncontrol
import android.view.LayoutInflater
import android.widget.EditText
import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AlertDialog
import androidx.navigation.NavController
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
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.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.feature.session.SessionUseCases
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.top.sites.TopSite
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.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.sessionsOfType
import org.mozilla.fenix.home.HomeFragment
import org.mozilla.fenix.home.HomeFragmentAction
import org.mozilla.fenix.home.HomeFragmentDirections
@ -173,11 +174,12 @@ class DefaultSessionControlController(
private val settings: Settings,
private val engine: Engine,
private val metrics: MetricController,
private val sessionManager: SessionManager,
private val store: BrowserStore,
private val tabCollectionStorage: TabCollectionStorage,
private val addTabUseCase: TabsUseCases.AddNewTabUseCase,
private val restoreUseCase: TabsUseCases.RestoreUseCase,
private val reloadUrlUseCase: SessionUseCases.ReloadUrlUseCase,
private val selectTabUseCase: TabsUseCases.SelectTabUseCase,
private val fragmentStore: HomeFragmentStore,
private val navController: NavController,
private val viewLifecycleScope: CoroutineScope,
@ -208,13 +210,15 @@ class DefaultSessionControlController(
override fun handleCollectionOpenTabClicked(tab: ComponentTab) {
dismissSearchDialogIfDisplayed()
sessionManager.restore(
restoreUseCase.invoke(
activity,
engine,
tab,
onTabRestored = {
activity.openToBrowser(BrowserDirection.FromHome)
reloadUrlUseCase.invoke(sessionManager.selectedSession)
selectTabUseCase.invoke(it)
reloadUrlUseCase.invoke(it)
},
onFailure = {
activity.openToBrowserAndLoad(
@ -229,7 +233,7 @@ class DefaultSessionControlController(
}
override fun handleCollectionOpenTabsTapped(collection: TabCollection) {
sessionManager.restore(
restoreUseCase.invoke(
activity,
engine,
collection,
@ -319,12 +323,9 @@ class DefaultSessionControlController(
setTitle(R.string.rename_top_site)
setView(customLayout)
setPositiveButton(R.string.top_sites_rename_dialog_ok) { dialog, _ ->
val newTitle = topSiteLabelEditText.text.toString()
if (newTitle.isNotBlank()) {
viewLifecycleScope.launch(Dispatchers.IO) {
with(activity.components.useCases.topSitesUseCase) {
renameTopSites(topSite, newTitle)
}
viewLifecycleScope.launch(Dispatchers.IO) {
with(activity.components.useCases.topSitesUseCase) {
renameTopSites(topSite, topSiteLabelEditText.text.toString())
}
}
dialog.dismiss()
@ -362,24 +363,71 @@ class DefaultSessionControlController(
override fun handleSelectTopSite(url: String, type: TopSite.Type) {
dismissSearchDialogIfDisplayed()
metrics.track(Event.TopSiteOpenInNewTab)
when (type) {
TopSite.Type.DEFAULT -> metrics.track(Event.TopSiteOpenDefault)
TopSite.Type.FRECENT -> metrics.track(Event.TopSiteOpenFrecent)
TopSite.Type.PINNED -> metrics.track(Event.TopSiteOpenPinned)
}
if (url == SupportUtils.GOOGLE_URL) {
metrics.track(Event.TopSiteOpenGoogle)
}
if (url == SupportUtils.POCKET_TRENDING_URL) {
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(
url = url,
url = appendSearchAttributionToUrlIfNeeded(url),
selectTab = true,
startLoading = true
)
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() {
if (navController.currentDestination?.id == R.id.searchDialogFragment) {
navController.navigateUp()
@ -436,8 +484,8 @@ class DefaultSessionControlController(
// Only register the observer right before moving to collection creation
registerCollectionStorageObserver()
val tabIds = sessionManager
.sessionsOfType(private = activity.browsingModeManager.mode.isPrivate)
val tabIds = store.state
.getNormalOrPrivateTabs(private = activity.browsingModeManager.mode.isPrivate)
.map { session -> session.id }
.toList()
.toTypedArray()

@ -13,6 +13,7 @@ import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
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.viewholders.topsites.TopSitesPagerAdapter
@ -28,7 +29,11 @@ class TopSitePagerViewHolder(
private val topSitesPageChangeCallback = object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
if (currentPage != position) {
pageIndicator.context.components.analytics.metrics.track(Event.TopSiteSwipeCarousel(position))
pageIndicator.context.components.analytics.metrics.track(
Event.TopSiteSwipeCarousel(
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>) {
val chunkedTopSites = topSites.chunked(TOP_SITES_PER_PAGE)
topSitesPagerAdapter.submitList(chunkedTopSites)

@ -21,7 +21,7 @@ class OnboardingPrivacyNoticeViewHolder(
view.header_text.setOnboardingIcon(R.drawable.ic_onboarding_privacy_notice)
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 {
it.context.components.analytics.metrics.track(Event.OnboardingPrivacyNotice)

@ -8,13 +8,14 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import kotlinx.android.synthetic.main.top_site_item.view.*
import mozilla.components.feature.top.sites.TopSite
import org.mozilla.fenix.home.sessioncontrol.TopSiteInteractor
import org.mozilla.fenix.perf.StartupTimeline
class TopSitesAdapter(
private val interactor: TopSiteInteractor
) : ListAdapter<TopSite, TopSiteItemViewHolder>(DiffCallback) {
) : ListAdapter<TopSite, TopSiteItemViewHolder>(TopSitesDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(TopSiteItemViewHolder.LAYOUT_ID, parent, false)
@ -26,11 +27,39 @@ class TopSitesAdapter(
holder.bind(getItem(position))
}
private object DiffCallback : DiffUtil.ItemCallback<TopSite>() {
override fun areItemsTheSame(oldItem: TopSite, newItem: TopSite) =
oldItem.id == newItem.id || oldItem.title == newItem.title || oldItem.url == newItem.url
override fun onBindViewHolder(
holder: TopSiteItemViewHolder,
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) =
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 kotlinx.android.synthetic.main.component_top_sites.view.*
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.viewholders.TopSiteViewHolder
class TopSitesPagerAdapter(
private val interactor: TopSiteInteractor
) : ListAdapter<List<TopSite>, TopSiteViewHolder>(DiffCallback) {
) : ListAdapter<List<TopSite>, TopSiteViewHolder>(TopSiteListDiffCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TopSiteViewHolder {
val view = LayoutInflater.from(parent.context)
@ -23,12 +24,30 @@ class TopSitesPagerAdapter(
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) {
val adapter = holder.itemView.top_sites_list.adapter as TopSitesAdapter
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 {
return oldItem.size == newItem.size
}
@ -36,5 +55,15 @@ class TopSitesPagerAdapter(
override fun areContentsTheSame(oldItem: List<TopSite>, newItem: List<TopSite>): Boolean {
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.ExperimentalCoroutinesApi
import kotlinx.coroutines.launch
import mozilla.components.browser.state.action.EngineAction
import mozilla.components.browser.state.action.RecentlyClosedAction
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.prompt.ShareData
import mozilla.components.lib.state.ext.consumeFrom
import mozilla.components.service.fxa.sync.SyncReason
@ -94,7 +96,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
::deleteHistoryItems,
::syncHistory,
requireComponents.analytics.metrics
)
)
historyInteractor = HistoryInteractor(
historyController
)
@ -271,6 +273,7 @@ class HistoryFragment : LibraryPageFragment<HistoryItem>(), UserInteractionHandl
requireComponents.analytics.metrics.track(Event.HistoryAllItemsRemoved)
requireComponents.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
requireComponents.core.historyStorage.deleteEverything()
deleteOpenTabsEngineHistory(requireComponents.core.store)
launch(Main) {
viewModel.invalidate()
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>) {
requireComponents.analytics.metrics.track(Event.HistoryItemShared)
val directions = HistoryFragmentDirections.actionGlobalShareFragment(

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

@ -9,12 +9,11 @@ import android.content.ClipboardManager
import android.content.res.Resources
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import mozilla.components.browser.session.SessionManager
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.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.HomeActivity
import org.mozilla.fenix.R
@ -22,29 +21,29 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
import org.mozilla.fenix.components.FenixSnackbar
interface RecentlyClosedController {
fun handleOpen(item: ClosedTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: ClosedTab)
fun handleCopyUrl(item: ClosedTab)
fun handleShare(item: ClosedTab)
fun handleOpen(item: RecoverableTab, mode: BrowsingMode? = null)
fun handleDeleteOne(tab: RecoverableTab)
fun handleCopyUrl(item: RecoverableTab)
fun handleShare(item: RecoverableTab)
fun handleNavigateToHistory()
fun handleRestore(item: ClosedTab)
fun handleRestore(item: RecoverableTab)
}
class DefaultRecentlyClosedController(
private val navController: NavController,
private val store: BrowserStore,
private val sessionManager: SessionManager,
private val tabsUseCases: TabsUseCases,
private val resources: Resources,
private val snackbar: FenixSnackbar,
private val clipboardManager: ClipboardManager,
private val activity: HomeActivity,
private val openToBrowser: (item: ClosedTab, mode: BrowsingMode?) -> Unit
private val openToBrowser: (item: RecoverableTab, mode: BrowsingMode?) -> Unit
) : RecentlyClosedController {
override fun handleOpen(item: ClosedTab, mode: BrowsingMode?) {
override fun handleOpen(item: RecoverableTab, mode: BrowsingMode?) {
openToBrowser(item, mode)
}
override fun handleDeleteOne(tab: ClosedTab) {
override fun handleDeleteOne(tab: RecoverableTab) {
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)
clipboardManager.setPrimaryClip(urlClipData)
with(snackbar) {
@ -64,7 +63,7 @@ class DefaultRecentlyClosedController(
}
}
override fun handleShare(item: ClosedTab) {
override fun handleShare(item: RecoverableTab) {
navController.navigate(
RecentlyClosedFragmentDirections.actionGlobalShareFragment(
data = arrayOf(ShareData(url = item.url, title = item.title))
@ -72,15 +71,15 @@ class DefaultRecentlyClosedController(
)
}
override fun handleRestore(item: ClosedTab) {
item.restoreTab(
store,
sessionManager,
onTabRestored = {
activity.openToBrowser(
from = BrowserDirection.FromRecentlyClosed
)
}
override fun handleRestore(item: RecoverableTab) {
tabsUseCases.restore(item)
store.dispatch(
RecentlyClosedAction.RemoveClosedTabAction(item)
)
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.flow.collect
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.flowScoped
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
@Suppress("TooManyFunctions")
class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
class RecentlyClosedFragment : LibraryPageFragment<RecoverableTab>() {
private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore
private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null
protected val recentlyClosedFragmentView: RecentlyClosedFragmentView
@ -82,7 +82,7 @@ class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
navController = findNavController(),
store = requireComponents.core.store,
activity = activity as HomeActivity,
sessionManager = requireComponents.core.sessionManager,
tabsUseCases = requireComponents.useCases.tabsUseCases,
resources = requireContext().resources,
snackbar = FenixSnackbar.make(
view = requireActivity().getRootView()!!,
@ -104,7 +104,7 @@ class RecentlyClosedFragment : LibraryPageFragment<ClosedTab>() {
_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 }
(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
import mozilla.components.browser.state.state.ClosedTab
import mozilla.components.browser.state.state.recover.RecoverableTab
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
/**
@ -14,27 +14,27 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingMode
class RecentlyClosedFragmentInteractor(
private val recentlyClosedController: RecentlyClosedController
) : RecentlyClosedInteractor {
override fun restore(item: ClosedTab) {
override fun restore(item: RecoverableTab) {
recentlyClosedController.handleRestore(item)
}
override fun onCopyPressed(item: ClosedTab) {
override fun onCopyPressed(item: RecoverableTab) {
recentlyClosedController.handleCopyUrl(item)
}
override fun onSharePressed(item: ClosedTab) {
override fun onSharePressed(item: RecoverableTab) {
recentlyClosedController.handleShare(item)
}
override fun onOpenInNormalTab(item: ClosedTab) {
override fun onOpenInNormalTab(item: RecoverableTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Normal)
}
override fun onOpenInPrivateTab(item: ClosedTab) {
override fun onOpenInPrivateTab(item: RecoverableTab) {
recentlyClosedController.handleOpen(item, BrowsingMode.Private)
}
override fun onDeleteOne(tab: ClosedTab) {
override fun onDeleteOne(tab: RecoverableTab) {
recentlyClosedController.handleDeleteOne(tab)
}

@ -4,7 +4,7 @@
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.State
import mozilla.components.lib.state.Store
@ -23,14 +23,14 @@ class RecentlyClosedFragmentStore(initialState: RecentlyClosedFragmentState) :
* `RecentlyClosedFragmentState` through the reducer.
*/
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
* @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.

@ -12,7 +12,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.android.extensions.LayoutContainer
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
interface RecentlyClosedInteractor {
@ -21,7 +21,7 @@ interface RecentlyClosedInteractor {
*
* @param item the tapped item to restore.
*/
fun restore(item: ClosedTab)
fun restore(item: RecoverableTab)
/**
* 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
*/
fun onCopyPressed(item: ClosedTab)
fun onCopyPressed(item: RecoverableTab)
/**
* Opens the share sheet for a recently closed tab item.
*
* @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.
*
* @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.
*
* @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.
*
* @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_list.isVisible = items.isNotEmpty()
recentlyClosedAdapter.submitList(items)

@ -7,7 +7,7 @@ package org.mozilla.fenix.library.recentlyclosed
import android.view.View
import androidx.recyclerview.widget.RecyclerView
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.library.history.HistoryItemMenu
import org.mozilla.fenix.utils.Do
@ -17,14 +17,14 @@ class RecentlyClosedItemViewHolder(
private val recentlyClosedFragmentInteractor: RecentlyClosedFragmentInteractor
) : RecyclerView.ViewHolder(view) {
private var item: ClosedTab? = null
private var item: RecoverableTab? = null
init {
setupMenu()
}
fun bind(
item: ClosedTab
item: RecoverableTab
) {
itemView.history_layout.titleView.text =
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.appcompat.app.AlertDialog
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.store.BrowserStore
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.support.ktx.kotlin.isUrl
import org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.HomeActivity
@ -40,7 +39,6 @@ interface SearchController {
fun handleSearchTermsTapped(searchTerms: String)
fun handleSearchShortcutEngineSelected(searchEngine: SearchEngine)
fun handleClickSearchEngineSettings()
fun handleExistingSessionSelected(session: Session)
fun handleExistingSessionSelected(tabId: String)
fun handleSearchShortcutsButtonClicked()
fun handleCameraPermissionsNeeded()
@ -49,8 +47,8 @@ interface SearchController {
@Suppress("TooManyFunctions", "LongParameterList")
class SearchDialogController(
private val activity: HomeActivity,
private val sessionManager: SessionManager,
private val store: BrowserStore,
private val tabsUseCases: TabsUseCases,
private val fragmentStore: SearchFragmentStore,
private val navController: NavController,
private val settings: Settings,
@ -74,12 +72,12 @@ class SearchDialogController(
navController.navigateSafe(R.id.searchDialogFragment, directions)
}
"moz://a" -> openSearchOrUrl(SupportUtils.getMozillaPageUrl(SupportUtils.MozillaPage.MANIFESTO))
else -> if (url.isNotBlank()) {
openSearchOrUrl(url)
} else {
dismissDialog()
}
else ->
if (url.isNotBlank()) {
openSearchOrUrl(url)
}
}
dismissDialog()
}
private fun openSearchOrUrl(url: String) {
@ -199,21 +197,16 @@ class SearchDialogController(
navController.navigateSafe(R.id.searchDialogFragment, directions)
}
override fun handleExistingSessionSelected(session: Session) {
override fun handleExistingSessionSelected(tabId: String) {
clearToolbarFocus()
sessionManager.select(session)
tabsUseCases.selectTab(tabId)
activity.openToBrowser(
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.
*

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

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

@ -4,7 +4,6 @@
package org.mozilla.fenix.search.awesomebar
import mozilla.components.browser.session.Session
import mozilla.components.browser.state.search.SearchEngine
/**
@ -36,11 +35,6 @@ interface AwesomeBarInteractor {
*/
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
*/

@ -86,7 +86,7 @@ class AwesomeBarView(
private val selectTabUseCase = object : TabsUseCases.SelectTabUseCase {
override fun invoke(session: Session) {
interactor.onExistingSessionSelected(session)
interactor.onExistingSessionSelected(session.id)
}
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
if (resultNotComputedFromCookies(trackKey) && hasValidCode(code, provider)) {
val channel = uri.getQueryParameter(CHANNEL_KEY)
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_SAP = "sap"
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(
var providerName: String,
var type: String,
var code: String?
var code: String?,
var channel: String? = null
) {
fun createTrackKey(): String {
return "${providerName.toLowerCase(Locale.ROOT)}.in-content" +
".${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() {
setSmallIcon(R.drawable.ic_private_browsing)
setContentTitle(getString(R.string.app_name_private_4, getString(R.string.app_name)))
setContentText(getString(R.string.notification_pbm_delete_text_2))
setContentTitle(applicationContext.getString(R.string.app_name_private_4, getString(R.string.app_name)))
setContentText(applicationContext.getString(R.string.notification_pbm_delete_text_2))
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) {
PhoneFeature.AUTOPLAY ->
throw IllegalAccessException("AUTOPLAY can't be accessed via get try " +
"using AUTOPLAY_AUDIBLE and AUTOPLAY_INAUDIBLE")
PhoneFeature.CAMERA -> camera
PhoneFeature.LOCATION -> location
PhoneFeature.MICROPHONE -> microphone
PhoneFeature.NOTIFICATION -> notification
PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible
PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible
PhoneFeature.AUTOPLAY_AUDIBLE -> autoplayAudible.toStatus()
PhoneFeature.AUTOPLAY_INAUDIBLE -> autoplayInaudible.toStatus()
PhoneFeature.PERSISTENT_STORAGE -> localStorage
PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS -> mediaKeySystemAccess
}
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.LOCATION -> copy(location = value)
PhoneFeature.MICROPHONE -> copy(microphone = value)
PhoneFeature.NOTIFICATION -> copy(notification = value)
PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value)
PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value)
PhoneFeature.AUTOPLAY_AUDIBLE -> copy(autoplayAudible = value.toAutoplayStatus())
PhoneFeature.AUTOPLAY_INAUDIBLE -> copy(autoplayInaudible = value.toAutoplayStatus())
PhoneFeature.PERSISTENT_STORAGE -> copy(localStorage = 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)),
MICROPHONE(arrayOf(RECORD_AUDIO)),
NOTIFICATION(emptyArray()),
AUTOPLAY(emptyArray()),
AUTOPLAY_AUDIBLE(emptyArray()),
AUTOPLAY_INAUDIBLE(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)
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)
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
MICROPHONE -> R.string.pref_key_phone_feature_microphone
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_INAUDIBLE -> R.string.pref_key_browser_feature_autoplay_inaudible
PERSISTENT_STORAGE -> R.string.pref_key_browser_feature_persistent_storage

@ -5,6 +5,8 @@
package org.mozilla.fenix.settings
import android.annotation.SuppressLint
import android.app.Activity
import android.app.role.RoleManager
import android.content.ActivityNotFoundException
import android.content.DialogInterface
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 org.mozilla.fenix.BrowserDirection
import org.mozilla.fenix.Config
import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.components.metrics.Event
@ -359,8 +360,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
val debuggingKey = getPreferenceKey(R.string.pref_key_remote_debugging)
val preferencePrivateBrowsing =
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 preferenceRemoteDebugging = findPreference<Preference>(debuggingKey)
val preferenceMakeDefaultBrowser =
@ -380,7 +379,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
}
}
preferenceExternalDownloadManager.isVisible = FeatureFlags.externalDownloadManager
preferenceRemoteDebugging?.isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
preferenceRemoteDebugging?.setOnPreferenceChangeListener<Boolean> { preference, newValue ->
preference.context.settings().preferences.edit()
@ -426,28 +424,66 @@ class SettingsFragment : PreferenceFragmentCompat() {
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 {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
Preference.OnPreferenceClickListener {
val intent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
startActivity(intent)
true
return when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> {
Preference.OnPreferenceClickListener {
requireContext().getSystemService(RoleManager::class.java).also {
if (!it.isRoleHeld(RoleManager.ROLE_BROWSER)) {
startActivityForResult(it.createRequestRoleIntent(RoleManager.ROLE_BROWSER), 0)
} else {
navigateUserToDefaultAppsSettings()
}
}
true
}
}
} else {
Preference.OnPreferenceClickListener {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
requireContext(),
SupportUtils.SumoTopic.SET_AS_DEFAULT_BROWSER
),
newTab = true,
from = BrowserDirection.FromSettings
)
true
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> {
Preference.OnPreferenceClickListener {
navigateUserToDefaultAppsSettings()
true
}
}
else -> {
Preference.OnPreferenceClickListener {
(activity as HomeActivity).openToBrowserAndLoad(
searchTermOrURL = SupportUtils.getSumoURLForTopic(
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() {
requirePreference<DefaultBrowserPreference>(R.string.pref_key_make_default_browser).updateSwitch()
}

@ -32,6 +32,8 @@ object SupportUtils {
"?e=&p=AyIGZRprFDJWWA1FBCVbV0IUWVALHFRBEwQAQB1AWQkFVUVXfFkAF14lRFRbJXstVWR3WQ1rJ08AZnhS" +
"HDJBYh4LZR9eEAMUBlccWCUBEQZRGFoXCxc3ZRteJUl8BmUZWhQ" +
"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) {
FENIX_MOVING("sync-delist"),

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

@ -12,6 +12,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.settings
/**
* 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
// the HomeActivity.
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.putExtra(HomeActivity.OPEN_TO_BROWSER, true)

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

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

@ -7,10 +7,12 @@ package org.mozilla.fenix.settings.deletebrowsingdata
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
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.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.concept.storage.HistoryStorage
import mozilla.components.feature.downloads.DownloadsUseCases
import mozilla.components.feature.tabs.TabsUseCases
import org.mozilla.fenix.components.PermissionStorage
import kotlin.coroutines.CoroutineContext
@ -21,10 +23,12 @@ interface DeleteBrowsingDataController {
suspend fun deleteCookies()
suspend fun deleteCachedFiles()
suspend fun deleteSitePermissions()
suspend fun deleteDownloads()
}
class DefaultDeleteBrowsingDataController(
private val removeAllTabs: TabsUseCases.RemoveAllTabsUseCase,
private val removeAllDownloads: DownloadsUseCases.RemoveAllDownloadsUseCase,
private val historyStorage: HistoryStorage,
private val permissionStorage: PermissionStorage,
private val store: BrowserStore,
@ -43,6 +47,7 @@ class DefaultDeleteBrowsingDataController(
withContext(coroutineContext) {
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
store.dispatch(EngineAction.PurgeHistoryAction)
iconsStorage.clear()
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
}
@ -75,4 +80,10 @@ class DefaultDeleteBrowsingDataController(
}
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.utils.Settings
@SuppressWarnings("TooManyFunctions")
@SuppressWarnings("TooManyFunctions", "LargeClass")
class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_data) {
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?) {
super.onViewCreated(view, savedInstanceState)
val tabsUseCases = requireComponents.useCases.tabsUseCases
val downloadUseCases = requireComponents.useCases.downloadUseCases
controller = DefaultDeleteBrowsingDataController(
requireComponents.useCases.tabsUseCases.removeAllTabs,
tabsUseCases.removeAllTabs,
downloadUseCases.removeAllDownloads,
requireComponents.core.historyStorage,
requireComponents.core.permissionStorage,
requireComponents.core.store,
@ -67,6 +69,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
R.id.cookies_item -> settings.deleteCookies
R.id.cached_files_item -> settings.deleteCache
R.id.site_permissions_item -> settings.deleteSitePermissions
R.id.downloads_item -> settings.deleteDownloads
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.cached_files_item -> settings.deleteCache = it.isChecked
R.id.site_permissions_item -> settings.deleteSitePermissions = it.isChecked
R.id.downloads_item -> settings.deleteDownloads = it.isChecked
else -> return
}
}
@ -151,6 +155,7 @@ class DeleteBrowsingDataFragment : Fragment(R.layout.fragment_delete_browsing_da
COOKIES_INDEX -> controller.deleteCookies()
CACHED_INDEX -> controller.deleteCachedFiles()
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.cookies_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 CACHED_INDEX = 3
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