diff --git a/.mergify.yml b/.mergify.yml index f4df54db1..1028efadc 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 9e51564f2..dc1be5113 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 diff --git a/app/metrics.yaml b/app/metrics.yaml index aee2119aa..5291a91ca 100644 --- a/app/metrics.yaml +++ b/app/metrics.yaml @@ -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" diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 3c44e42bf..b7a3d200f 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -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(...); } diff --git a/app/src/androidTest/assets/pages/download.html b/app/src/androidTest/assets/pages/download.html index f6dd187d5..440a3b0c0 100644 --- a/app/src/androidTest/assets/pages/download.html +++ b/app/src/androidTest/assets/pages/download.html @@ -1,5 +1,10 @@ - Page content: Globe.svg + Page content: Globe.svg + diff --git a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt index dc7521062..ad339c592 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/helpers/TestHelper.kt @@ -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() + } + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt index 7df918cb1..3575a7101 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/perf/StartupExcessiveResourceUseTest.kt @@ -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) } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt index 8be03fb46..dcf5a6c4f 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/BookmarksTest.kt @@ -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) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt.ignore b/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt.ignore index 1c9074de3..81b62c4f7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt.ignore +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/CollectionTest.kt.ignore @@ -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) // diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt index c0919cce8..92774c7aa 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/DownloadTest.kt @@ -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 { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt index ae9c165e6..b29837983 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/NavigationToolbarTest.kt @@ -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()) } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt index e73ac0182..d7e0c50b8 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/ReaderViewTest.kt @@ -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 { diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt index 5345f38cb..14d03757a 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SettingsBasicsTest.kt @@ -120,6 +120,7 @@ class SettingsBasicsTest { navigationToolbar { }.enterURLAndEnterToBrowser(page2.url) { + verifyUrl(page2.url.toString()) }.openThreeDotMenu { clickAddBookmarkButton() } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt index 881855d37..28efbb031 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/SmokeTest.kt @@ -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) + } + } } diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt index bfb6b1ce9..7a7219ef7 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/AddToHomeScreenRobot.kt @@ -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))) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt index 2d2c8fda7..8eddc0384 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BookmarksRobot.kt @@ -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())) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt index 99be574f0..efce62eec 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -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)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt index c771602a5..aca0e0bf0 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/DownloadRobot.kt @@ -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())) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt index 428f0bfc6..7c156b5b2 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/HomeScreenRobot.kt @@ -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) = diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt index 1a66130a9..3c63c7eb9 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/NavigationToolbarRobot.kt @@ -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) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt index 840a011de..8532d5af3 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ReaderViewRobot.kt @@ -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) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt new file mode 100644 index 000000000..c2bbd5cad --- /dev/null +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/RecentlyClosedTabsRobot.kt @@ -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")) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt index 431680d24..07f36c0ff 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuDataCollectionRobot.kt @@ -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)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt index 94e857a83..da518f563 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/SettingsSubMenuSearchRobot.kt @@ -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))) + ) + ) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt index a14b0e539..16dddca95 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/TabDrawerRobot.kt @@ -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)) diff --git a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt index bbcd60285..da61cb380 100644 --- a/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt +++ b/app/src/androidTest/java/org/mozilla/fenix/ui/robots/ThreeDotMenuMainRobot.kt @@ -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())) +} diff --git a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt index 063a73cf2..1e4d8926a 100644 --- a/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt +++ b/app/src/main/java/org/mozilla/fenix/AppRequestInterceptor.kt @@ -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)" } } diff --git a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt index 8e16726f5..1a9871792 100644 --- a/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt +++ b/app/src/main/java/org/mozilla/fenix/FeatureFlags.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index baf88849a..f76b5ffb3 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 5ca572000..64cfb7740 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt index f3b18382f..426501046 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/AddonsManagementFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt index c0b135a60..ece5d82c2 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/Extensions.kt @@ -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() - } -} diff --git a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt index 345db7f35..c3fbcce10 100644 --- a/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/addons/InstalledAddonDetailsFragment.kt @@ -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. diff --git a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt index 4df9101bf..60dfe78f3 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt @@ -86,7 +86,6 @@ import org.mozilla.fenix.HomeActivity import org.mozilla.fenix.IntentReceiverActivity import org.mozilla.fenix.NavGraphDirections import org.mozilla.fenix.OnBackLongPressedListener -import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.R import org.mozilla.fenix.browser.browsingmode.BrowsingMode import org.mozilla.fenix.browser.readermode.DefaultReaderModeController @@ -106,7 +105,6 @@ import org.mozilla.fenix.components.toolbar.ToolbarIntegration import org.mozilla.fenix.components.toolbar.ToolbarPosition import org.mozilla.fenix.downloads.DownloadService import org.mozilla.fenix.downloads.DynamicDownloadDialog -import org.mozilla.fenix.ext.getPreferenceKey import org.mozilla.fenix.ext.accessibilityManager import org.mozilla.fenix.ext.breadcrumb import org.mozilla.fenix.ext.components @@ -117,6 +115,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.HomeScreenViewModel import org.mozilla.fenix.home.SharedViewModel import org.mozilla.fenix.onboarding.FenixOnboarding @@ -127,8 +126,6 @@ import org.mozilla.fenix.wifi.SitePermissionsWifiIntegration import java.lang.ref.WeakReference import mozilla.components.feature.media.fullscreen.MediaFullscreenOrientationFeature import org.mozilla.fenix.FeatureFlags.newMediaSessionApi -import org.mozilla.fenix.settings.PhoneFeature -import org.mozilla.fenix.settings.quicksettings.QuickSettingsSheetDialogFragmentDirections /** * Base fragment extended by [BrowserFragment]. @@ -176,6 +173,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, private var fullScreenMediaSessionFeature = ViewBoundFeatureWrapper() private val searchFeature = ViewBoundFeatureWrapper() + private val webAuthnFeature = ViewBoundFeatureWrapper() private var pipFeature: PictureInPictureFeature? = null var customTabSessionId: String? = null @@ -186,6 +184,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, protected var webAppToolbarShouldBeVisible = true private val sharedViewModel: SharedViewModel by activityViewModels() + private val homeViewModel: HomeScreenViewModel by activityViewModels() @VisibleForTesting internal val onboarding by lazy { FenixOnboarding(requireContext()) } @@ -222,7 +221,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } final override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - browserInitialized = initializeUI(view) != null + initializeUI(view) if (customTabSessionId == null) { // We currently only need this observer to navigate to home @@ -240,12 +239,19 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, requireContext().accessibilityManager.addAccessibilityStateChangeListener(this) } - private val homeViewModel: HomeScreenViewModel by activityViewModels() + private fun initializeUI(view: View) { + val tab = getCurrentTab() + browserInitialized = if (tab != null) { + initializeUI(view, tab) + true + } else { + false + } + } @Suppress("ComplexMethod", "LongMethod") @CallSuper - @VisibleForTesting - internal open fun initializeUI(view: View): Session? { + internal open fun initializeUI(view: View, tab: SessionState) { val context = requireContext() val sessionManager = context.components.core.sessionManager val store = context.components.core.store @@ -262,449 +268,454 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, beginAnimateInIfNecessary() } - return getSessionById()?.also { _ -> - val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { - action = Intent.ACTION_VIEW - putExtra(HomeActivity.OPEN_TO_BROWSER, true) - } - - val readerMenuController = DefaultReaderModeController( - readerViewFeature, - view.readerViewControlsBar, - isPrivate = activity.browsingModeManager.mode.isPrivate - ) - val browserToolbarController = DefaultBrowserToolbarController( - store = store, - activity = activity, - navController = findNavController(), - metrics = requireComponents.analytics.metrics, - readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, - engineView = engineView, - homeViewModel = homeViewModel, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - onTabCounterClicked = { - thumbnailsFeature.get()?.requestScreenshot() - findNavController().nav( - R.id.browserFragment, - BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() - ) - }, - onCloseTab = { closedSession -> - val tab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController + val openInFenixIntent = Intent(context, IntentReceiverActivity::class.java).apply { + action = Intent.ACTION_VIEW + putExtra(HomeActivity.OPEN_TO_BROWSER, true) + } - val snackbarMessage = if (tab.content.private) { - requireContext().getString(R.string.snackbar_private_tab_closed) - } else { - requireContext().getString(R.string.snackbar_tab_closed) - } + val readerMenuController = DefaultReaderModeController( + readerViewFeature, + view.readerViewControlsBar, + isPrivate = activity.browsingModeManager.mode.isPrivate + ) + val browserToolbarController = DefaultBrowserToolbarController( + store = store, + activity = activity, + navController = findNavController(), + metrics = requireComponents.analytics.metrics, + readerModeController = readerMenuController, + sessionManager = requireComponents.core.sessionManager, + engineView = engineView, + homeViewModel = homeViewModel, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + onTabCounterClicked = { + thumbnailsFeature.get()?.requestScreenshot() + findNavController().nav( + R.id.browserFragment, + BrowserFragmentDirections.actionGlobalTabTrayDialogFragment() + ) + }, + onCloseTab = { closedSession -> + val closedTab = store.state.findTab(closedSession.id) ?: return@DefaultBrowserToolbarController + + val snackbarMessage = if (closedTab.content.private) { + requireContext().getString(R.string.snackbar_private_tab_closed) + } else { + requireContext().getString(R.string.snackbar_tab_closed) + } - viewLifecycleOwner.lifecycleScope.allowUndo( - requireView().browserLayout, - snackbarMessage, - requireContext().getString(R.string.snackbar_deleted_undo), - { - requireComponents.useCases.tabsUseCases.undo.invoke() - }, - paddedForBottomToolbar = true, - operation = { } - ) + viewLifecycleOwner.lifecycleScope.allowUndo( + requireView().browserLayout, + snackbarMessage, + requireContext().getString(R.string.snackbar_deleted_undo), + { + requireComponents.useCases.tabsUseCases.undo.invoke() + }, + paddedForBottomToolbar = true, + operation = { } + ) + } + ) + val browserToolbarMenuController = DefaultBrowserToolbarMenuController( + activity = activity, + navController = findNavController(), + metrics = requireComponents.analytics.metrics, + settings = context.settings(), + readerModeController = readerMenuController, + sessionManager = requireComponents.core.sessionManager, + sessionFeature = sessionFeature, + findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, + swipeRefresh = swipeRefresh, + browserAnimator = browserAnimator, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + openInFenixIntent = openInFenixIntent, + bookmarkTapped = { url: String, title: String -> + viewLifecycleOwner.lifecycleScope.launch { + bookmarkTapped(url, title) } - ) - val browserToolbarMenuController = DefaultBrowserToolbarMenuController( - activity = activity, - navController = findNavController(), - metrics = requireComponents.analytics.metrics, - settings = context.settings(), - readerModeController = readerMenuController, - sessionManager = requireComponents.core.sessionManager, - sessionFeature = sessionFeature, - findInPageLauncher = { findInPageIntegration.withFeature { it.launch() } }, - swipeRefresh = swipeRefresh, - browserAnimator = browserAnimator, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - openInFenixIntent = openInFenixIntent, - bookmarkTapped = { url: String, title: String -> - viewLifecycleOwner.lifecycleScope.launch { - bookmarkTapped(url, title) - } - }, - scope = viewLifecycleOwner.lifecycleScope, - tabCollectionStorage = requireComponents.core.tabCollectionStorage, - topSitesStorage = requireComponents.core.topSitesStorage, - browserStore = store - ) + }, + scope = viewLifecycleOwner.lifecycleScope, + tabCollectionStorage = requireComponents.core.tabCollectionStorage, + topSitesStorage = requireComponents.core.topSitesStorage, + browserStore = store + ) - _browserInteractor = BrowserInteractor( - browserToolbarController, - browserToolbarMenuController - ) + _browserInteractor = BrowserInteractor( + browserToolbarController, + browserToolbarMenuController + ) - _browserToolbarView = BrowserToolbarView( - container = view.browserLayout, - toolbarPosition = context.settings().toolbarPosition, - interactor = browserInteractor, - customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, - lifecycleOwner = viewLifecycleOwner - ) + _browserToolbarView = BrowserToolbarView( + container = view.browserLayout, + toolbarPosition = context.settings().toolbarPosition, + interactor = browserInteractor, + customTabSession = customTabSessionId?.let { sessionManager.findSessionById(it) }, + lifecycleOwner = viewLifecycleOwner + ) - toolbarIntegration.set( - feature = browserToolbarView.toolbarIntegration, - owner = this, - view = view - ) + toolbarIntegration.set( + feature = browserToolbarView.toolbarIntegration, + owner = this, + view = view + ) - findInPageIntegration.set( - feature = FindInPageIntegration( - store = store, - sessionId = customTabSessionId, - stub = view.stubFindInPage, - engineView = view.engineView, - toolbar = browserToolbarView.view - ), - owner = this, - view = view - ) + findInPageIntegration.set( + feature = FindInPageIntegration( + store = store, + sessionId = customTabSessionId, + stub = view.stubFindInPage, + engineView = view.engineView, + toolbar = browserToolbarView.view + ), + owner = this, + view = view + ) - browserToolbarView.view.display.setOnSiteSecurityClickedListener { - showQuickSettingsDialog() - } + browserToolbarView.view.display.setOnSiteSecurityClickedListener { + showQuickSettingsDialog() + } - browserToolbarView.view.display.setOnPermissionIndicatorClickedListener { - navigateToAutoplaySetting() - } + browserToolbarView.view.display.setOnTrackingProtectionClickedListener { + context.metrics.track(Event.TrackingProtectionIconPressed) + showTrackingProtectionPanel() + } - browserToolbarView.view.display.setOnTrackingProtectionClickedListener { - context.metrics.track(Event.TrackingProtectionIconPressed) - showTrackingProtectionPanel() - } + contextMenuFeature.set( + feature = ContextMenuFeature( + fragmentManager = parentFragmentManager, + store = store, + candidates = getContextMenuCandidates(context, view.browserLayout), + engineView = view.engineView, + useCases = context.components.useCases.contextMenuUseCases, + tabId = customTabSessionId + ), + owner = this, + view = view + ) - contextMenuFeature.set( - feature = ContextMenuFeature( - fragmentManager = parentFragmentManager, - store = store, - candidates = getContextMenuCandidates(context, view.browserLayout), - engineView = view.engineView, - useCases = context.components.useCases.contextMenuUseCases, - tabId = customTabSessionId + val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode + secureWindowFeature.set( + feature = SecureWindowFeature( + window = requireActivity().window, + store = store, + customTabId = customTabSessionId, + isSecure = { !allowScreenshotsInPrivateMode && it.content.private } + ), + owner = this, + view = view + ) + + if (newMediaSessionApi) { + fullScreenMediaSessionFeature.set( + feature = MediaSessionFullscreenFeature( + requireActivity(), + context.components.core.store ), owner = this, view = view ) - - val allowScreenshotsInPrivateMode = context.settings().allowScreenshotsInPrivateMode - secureWindowFeature.set( - feature = SecureWindowFeature( - window = requireActivity().window, - store = store, - customTabId = customTabSessionId, - isSecure = { !allowScreenshotsInPrivateMode && it.content.private } + } else { + fullScreenMediaFeature.set( + feature = MediaFullscreenOrientationFeature( + requireActivity(), + context.components.core.store ), owner = this, view = view ) + } - if (newMediaSessionApi) { - fullScreenMediaSessionFeature.set( - feature = MediaSessionFullscreenFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view - ) - } else { - fullScreenMediaFeature.set( - feature = MediaFullscreenOrientationFeature( - requireActivity(), - context.components.core.store - ), - owner = this, - view = view - ) - } - - val downloadFeature = DownloadsFeature( + val downloadFeature = DownloadsFeature( + context.applicationContext, + store = store, + useCases = context.components.useCases.downloadUseCases, + fragmentManager = childFragmentManager, + tabId = customTabSessionId, + downloadManager = FetchDownloadManager( context.applicationContext, - store = store, - useCases = context.components.useCases.downloadUseCases, - fragmentManager = childFragmentManager, - tabId = customTabSessionId, - downloadManager = FetchDownloadManager( - context.applicationContext, - store, - DownloadService::class + store, + DownloadService::class + ), + shouldForwardToThirdParties = { + PreferenceManager.getDefaultSharedPreferences(context).getBoolean( + context.getPreferenceKey(R.string.pref_key_external_download_manager), false + ) + }, + promptsStyling = DownloadsFeature.PromptsStyling( + gravity = Gravity.BOTTOM, + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = ThemeManager.resolveAttribute( + R.attr.accent, + context ), - shouldForwardToThirdParties = { - PreferenceManager.getDefaultSharedPreferences(context).getBoolean( - context.getPreferenceKey(R.string.pref_key_external_download_manager), false - ) - }, - promptsStyling = DownloadsFeature.PromptsStyling( - gravity = Gravity.BOTTOM, - shouldWidthMatchParent = true, - positiveButtonBackgroundColor = ThemeManager.resolveAttribute( - R.attr.accent, - context - ), - positiveButtonTextColor = ThemeManager.resolveAttribute( - R.attr.contrastText, - context - ), - positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat() + positiveButtonTextColor = ThemeManager.resolveAttribute( + R.attr.contrastText, + context ), - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) - } - ) + positiveButtonRadius = (resources.getDimensionPixelSize(R.dimen.tab_corner_radius)).toFloat() + ), + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_DOWNLOAD_PERMISSIONS) + } + ) - downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> - // If the download is just paused, don't show any in-app notification - if (downloadJobStatus == DownloadState.Status.COMPLETED || - downloadJobStatus == DownloadState.Status.FAILED - ) { + downloadFeature.onDownloadStopped = { downloadState, _, downloadJobStatus -> + // If the download is just paused, don't show any in-app notification + if (downloadJobStatus == DownloadState.Status.COMPLETED || + downloadJobStatus == DownloadState.Status.FAILED + ) { - saveDownloadDialogState( - downloadState.sessionId, - downloadState, - downloadJobStatus - ) + saveDownloadDialogState( + downloadState.sessionId, + downloadState, + downloadJobStatus + ) - val dynamicDownloadDialog = DynamicDownloadDialog( - container = view.browserLayout, - downloadState = downloadState, - metrics = requireComponents.analytics.metrics, - didFail = downloadJobStatus == DownloadState.Status.FAILED, - tryAgain = downloadFeature::tryAgain, - onCannotOpenFile = { - FenixSnackbar.make( - view = view.browserLayout, - duration = Snackbar.LENGTH_SHORT, - isDisplayedWithBrowserToolbar = true - ) - .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file)) - .show() - }, - view = view.viewDynamicDownloadDialog, - toolbarHeight = toolbarHeight, - onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } - ) + val dynamicDownloadDialog = DynamicDownloadDialog( + container = view.browserLayout, + downloadState = downloadState, + metrics = requireComponents.analytics.metrics, + didFail = downloadJobStatus == DownloadState.Status.FAILED, + tryAgain = downloadFeature::tryAgain, + onCannotOpenFile = { + FenixSnackbar.make( + view = view.browserLayout, + duration = Snackbar.LENGTH_SHORT, + isDisplayedWithBrowserToolbar = true + ) + .setText(context.getString(R.string.mozac_feature_downloads_could_not_open_file)) + .show() + }, + view = view.viewDynamicDownloadDialog, + toolbarHeight = toolbarHeight, + onDismiss = { sharedViewModel.downloadDialogState.remove(downloadState.sessionId) } + ) - // Don't show the dialog if we aren't in the tab that started the download - if (downloadState.sessionId == sessionManager.selectedSession?.id) { - dynamicDownloadDialog.show() - browserToolbarView.expand() - } + // Don't show the dialog if we aren't in the tab that started the download + if (downloadState.sessionId == sessionManager.selectedSession?.id) { + dynamicDownloadDialog.show() + browserToolbarView.expand() } } + } - resumeDownloadDialogState( - sessionManager.selectedSession?.id, - store, view, context, toolbarHeight - ) + resumeDownloadDialogState( + sessionManager.selectedSession?.id, + store, view, context, toolbarHeight + ) - downloadsFeature.set( - downloadFeature, - owner = this, - view = view - ) + downloadsFeature.set( + downloadFeature, + owner = this, + view = view + ) - pipFeature = PictureInPictureFeature( + pipFeature = PictureInPictureFeature( + store = store, + activity = requireActivity(), + crashReporting = context.components.analytics.crashReporter, + tabId = customTabSessionId + ) + + appLinksFeature.set( + feature = AppLinksFeature( + context, store = store, - activity = requireActivity(), - crashReporting = context.components.analytics.crashReporter, - tabId = customTabSessionId - ) + sessionId = customTabSessionId, + fragmentManager = parentFragmentManager, + launchInApp = { context.settings().openLinksInExternalApp }, + loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl + ), + owner = this, + view = view + ) - appLinksFeature.set( - feature = AppLinksFeature( - context, - sessionManager = sessionManager, - sessionId = customTabSessionId, - fragmentManager = parentFragmentManager, - launchInApp = { context.settings().openLinksInExternalApp }, - loadUrlUseCase = context.components.useCases.sessionUseCases.loadUrl + promptsFeature.set( + feature = PromptFeature( + fragment = this, + store = store, + customTabId = customTabSessionId, + fragmentManager = parentFragmentManager, + loginValidationDelegate = DefaultLoginValidationDelegate( + context.components.core.lazyPasswordsStorage ), - owner = this, - view = view - ) - - promptsFeature.set( - feature = PromptFeature( - fragment = this, - store = store, - customTabId = customTabSessionId, - fragmentManager = parentFragmentManager, - loginValidationDelegate = DefaultLoginValidationDelegate( - context.components.core.lazyPasswordsStorage - ), - isSaveLoginEnabled = { - context.settings().shouldPromptToSaveLogins - }, - loginExceptionStorage = context.components.core.loginExceptionStorage, - shareDelegate = object : ShareDelegate { - override fun showShareSheet( - context: Context, - shareData: ShareData, - onDismiss: () -> Unit, - onSuccess: () -> Unit - ) { - val directions = NavGraphDirections.actionGlobalShareFragment( - data = arrayOf(shareData), - showPage = true, - sessionId = getSessionById()?.id - ) - findNavController().navigate(directions) - } - }, - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) - }, - loginPickerView = loginSelectBar, - onManageLogins = { - browserAnimator.captureEngineViewAndDrawStatically { - val directions = - NavGraphDirections.actionGlobalSavedLoginsAuthFragment() - findNavController().navigate(directions) - } + isSaveLoginEnabled = { + context.settings().shouldPromptToSaveLogins + }, + loginExceptionStorage = context.components.core.loginExceptionStorage, + shareDelegate = object : ShareDelegate { + override fun showShareSheet( + context: Context, + shareData: ShareData, + onDismiss: () -> Unit, + onSuccess: () -> Unit + ) { + val directions = NavGraphDirections.actionGlobalShareFragment( + data = arrayOf(shareData), + showPage = true, + sessionId = getSessionById()?.id + ) + findNavController().navigate(directions) } - ), - owner = this, - view = view - ) + }, + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS) + }, + loginPickerView = loginSelectBar, + onManageLogins = { + browserAnimator.captureEngineViewAndDrawStatically { + val directions = + NavGraphDirections.actionGlobalSavedLoginsAuthFragment() + findNavController().navigate(directions) + } + } + ), + owner = this, + view = view + ) - sessionFeature.set( - feature = SessionFeature( - requireComponents.core.store, - requireComponents.useCases.sessionUseCases.goBack, - view.engineView, - customTabSessionId - ), - owner = this, - view = view - ) + sessionFeature.set( + feature = SessionFeature( + requireComponents.core.store, + requireComponents.useCases.sessionUseCases.goBack, + view.engineView, + customTabSessionId + ), + owner = this, + view = view + ) - searchFeature.set( - feature = SearchFeature(store, customTabSessionId) { request, tabId -> - val parentSession = sessionManager.findSessionById(tabId) - val useCase = if (request.isPrivate) { - requireComponents.useCases.searchUseCases.newPrivateTabSearch - } else { - requireComponents.useCases.searchUseCases.newTabSearch - } + searchFeature.set( + feature = SearchFeature(store, customTabSessionId) { request, tabId -> + val parentSession = sessionManager.findSessionById(tabId) + val useCase = if (request.isPrivate) { + requireComponents.useCases.searchUseCases.newPrivateTabSearch + } else { + requireComponents.useCases.searchUseCases.newTabSearch + } - if (parentSession?.isCustomTabSession() == true) { - useCase.invoke(request.query) - requireActivity().startActivity(openInFenixIntent) - } else { - useCase.invoke(request.query, parentSessionId = parentSession?.id) - } - }, - owner = this, - view = view - ) + if (parentSession?.isCustomTabSession() == true) { + useCase.invoke(request.query) + requireActivity().startActivity(openInFenixIntent) + } else { + useCase.invoke(request.query, parentSessionId = parentSession?.id) + } + }, + owner = this, + view = view + ) - val accentHighContrastColor = - ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) - - sitePermissionsFeature.set( - feature = SitePermissionsFeature( - context = context, - storage = context.components.core.permissionStorage.permissionsStorage, - fragmentManager = parentFragmentManager, - promptsStyling = SitePermissionsFeature.PromptsStyling( - gravity = getAppropriateLayoutGravity(), - shouldWidthMatchParent = true, - positiveButtonBackgroundColor = accentHighContrastColor, - positiveButtonTextColor = R.color.photonWhite - ), - sessionId = customTabSessionId, - onNeedToRequestPermissions = { permissions -> - requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS) - }, - onShouldShowRequestPermissionRationale = { - shouldShowRequestPermissionRationale( - it - ) - }, - store = store - ), - owner = this, - view = view - ) + val accentHighContrastColor = + ThemeManager.resolveAttribute(R.attr.accentHighContrast, context) - sitePermissionWifiIntegration.set( - feature = SitePermissionsWifiIntegration( - settings = context.settings(), - wifiConnectionMonitor = context.components.wifiConnectionMonitor + sitePermissionsFeature.set( + feature = SitePermissionsFeature( + context = context, + storage = context.components.core.permissionStorage.permissionsStorage, + fragmentManager = parentFragmentManager, + promptsStyling = SitePermissionsFeature.PromptsStyling( + gravity = getAppropriateLayoutGravity(), + shouldWidthMatchParent = true, + positiveButtonBackgroundColor = accentHighContrastColor, + positiveButtonTextColor = R.color.photonWhite ), - owner = this, - view = view - ) + sessionId = customTabSessionId, + onNeedToRequestPermissions = { permissions -> + requestPermissions(permissions, REQUEST_CODE_APP_PERMISSIONS) + }, + onShouldShowRequestPermissionRationale = { + shouldShowRequestPermissionRationale( + it + ) + }, + store = store + ), + owner = this, + view = view + ) - context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { - // If the user connects to WIFI while on the BrowserFragment, this will update the - // SitePermissionsRules (specifically autoplay) accordingly - runIfFragmentIsAttached { - assignSitePermissionsRules() - } - } - assignSitePermissionsRules() + sitePermissionWifiIntegration.set( + feature = SitePermissionsWifiIntegration( + settings = context.settings(), + wifiConnectionMonitor = context.components.wifiConnectionMonitor + ), + owner = this, + view = view + ) - fullScreenFeature.set( - feature = FullScreenFeature( - requireComponents.core.store, - requireComponents.useCases.sessionUseCases, - customTabSessionId, - ::viewportFitChange, - ::fullScreenChanged + if (FeatureFlags.webAuthFeature) { + webAuthnFeature.set( + feature = WebAuthnFeature( + engine = requireComponents.core.engine, + activity = requireActivity() ), owner = this, view = view ) + } - expandToolbarOnNavigation(store) - - store.flowScoped(viewLifecycleOwner) { flow -> - flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) } - .ifChanged { tab -> tab.content.pictureInPictureEnabled } - .collect { tab -> pipModeChanged(tab) } + context.settings().setSitePermissionSettingListener(viewLifecycleOwner) { + // If the user connects to WIFI while on the BrowserFragment, this will update the + // SitePermissionsRules (specifically autoplay) accordingly + runIfFragmentIsAttached { + assignSitePermissionsRules() } + } + assignSitePermissionsRules() + + fullScreenFeature.set( + feature = FullScreenFeature( + requireComponents.core.store, + requireComponents.useCases.sessionUseCases, + customTabSessionId, + ::viewportFitChange, + ::fullScreenChanged + ), + owner = this, + view = view + ) - view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled() - - if (view.swipeRefresh.isEnabled) { - val primaryTextColor = - ThemeManager.resolveAttribute(R.attr.primaryText, context) - view.swipeRefresh.setColorSchemeColors(primaryTextColor) - swipeRefreshFeature.set( - feature = SwipeRefreshFeature( - requireComponents.core.store, - context.components.useCases.sessionUseCases.reload, - view.swipeRefresh, - customTabSessionId - ), - owner = this, - view = view - ) - } + expandToolbarOnNavigation(store) + + store.flowScoped(viewLifecycleOwner) { flow -> + flow.mapNotNull { state -> state.findTabOrCustomTabOrSelectedTab(customTabSessionId) } + .ifChanged { tab -> tab.content.pictureInPictureEnabled } + .collect { tab -> pipModeChanged(tab) } + } - webchannelIntegration.set( - feature = FxaWebChannelFeature( - requireContext(), - customTabSessionId, - requireComponents.core.engine, + view.swipeRefresh.isEnabled = shouldPullToRefreshBeEnabled(false) + + if (view.swipeRefresh.isEnabled) { + val primaryTextColor = + ThemeManager.resolveAttribute(R.attr.primaryText, context) + view.swipeRefresh.setColorSchemeColors(primaryTextColor) + swipeRefreshFeature.set( + feature = SwipeRefreshFeature( requireComponents.core.store, - requireComponents.backgroundServices.accountManager, - requireComponents.backgroundServices.serverConfig, - setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC) + context.components.useCases.sessionUseCases.reload, + view.swipeRefresh, + customTabSessionId ), owner = this, view = view ) - - initializeEngineView(toolbarHeight) } + + webchannelIntegration.set( + feature = FxaWebChannelFeature( + requireContext(), + customTabSessionId, + requireComponents.core.engine, + requireComponents.core.store, + requireComponents.backgroundServices.accountManager, + requireComponents.backgroundServices.serverConfig, + setOf(FxaCapability.CHOOSE_WHAT_TO_SYNC) + ), + owner = this, + view = view + ) + + initializeEngineView(toolbarHeight) } @VisibleForTesting @@ -801,10 +812,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } @VisibleForTesting - internal fun shouldPullToRefreshBeEnabled(): Boolean { + internal fun shouldPullToRefreshBeEnabled(inFullScreen: Boolean): Boolean { return FeatureFlags.pullToRefreshEnabled && requireContext().settings().isPullToRefreshEnabledInBrowser && - !(requireActivity() as HomeActivity).isImmersive + !inFullScreen } private fun initializeEngineView(toolbarHeight: Int) { @@ -923,9 +934,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, resumeDownloadDialogState(selectedTab.id, context.components.core.store, view, context, toolbarHeight) } } else { - view?.let { view -> - browserInitialized = initializeUI(view) != null - } + view?.let { view -> initializeUI(view) } } } @@ -972,6 +981,7 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, override fun onBackPressed(): Boolean { return findInPageIntegration.onBackPressed() || fullScreenFeature.onBackPressed() || + promptsFeature.onBackPressed() || sessionFeature.onBackPressed() || removeSessionIfNeeded() } @@ -1026,7 +1036,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, * Forwards activity results to the prompt feature. */ final override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - promptsFeature.withFeature { it.onActivityResult(requestCode, resultCode, data) } + listOf( + promptsFeature, + webAuthnFeature + ).any { it.onActivityResult(requestCode, data, resultCode) } } /** @@ -1057,11 +1070,11 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } protected abstract fun navToQuickSettingsSheet( - session: Session, + tab: SessionState, sitePermissions: SitePermissions? ) - protected abstract fun navToTrackingProtectionPanel(session: Session) + protected abstract fun navToTrackingProtectionPanel(tab: SessionState) /** * Returns the layout [android.view.Gravity] for the quick settings and ETP dialog. @@ -1085,23 +1098,23 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, * which lets the user control tracking protection and site settings. */ private fun showQuickSettingsDialog() { - val session = getSessionById() ?: return + val tab = getCurrentTab() ?: return viewLifecycleOwner.lifecycleScope.launch(Main) { - val sitePermissions: SitePermissions? = session.url.toUri().host?.let { host -> + val sitePermissions: SitePermissions? = tab.content.url.toUri().host?.let { host -> val storage = requireComponents.core.permissionStorage storage.findSitePermissionsBy(host) } view?.let { - navToQuickSettingsSheet(session, sitePermissions) + navToQuickSettingsSheet(tab, sitePermissions) } } } private fun showTrackingProtectionPanel() { - val session = getSessionById() ?: return + val tab = getCurrentTab() ?: return view?.let { - navToTrackingProtectionPanel(session) + navToTrackingProtectionPanel(tab) } } @@ -1127,6 +1140,10 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } } + private fun getCurrentTab(): SessionState? { + return requireComponents.core.store.state.findCustomTabOrSelectedTab(customTabSessionId) + } + private suspend fun bookmarkTapped(sessionUrl: String, sessionTitle: String) = withContext(IO) { val bookmarksStorage = requireComponents.core.bookmarksStorage val existing = @@ -1232,6 +1249,8 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, initializeEngineView(toolbarHeight) } } + + activity?.swipeRefresh?.isEnabled = shouldPullToRefreshBeEnabled(inFullScreen) } /* @@ -1295,12 +1314,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, } } - private fun navigateToAutoplaySetting() { - val directions = QuickSettingsSheetDialogFragmentDirections - .actionGlobalSitePermissionsManagePhoneFeature(PhoneFeature.AUTOPLAY_AUDIBLE) - findNavController().navigate(directions) - } - // This method is called in response to native web extension messages from // content scripts (e.g the reader view extension). By the time these // messages are processed the fragment/view may no longer be attached. diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 22ebead2d..042d33a3b 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -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() + private val openInAppOnboardingObserver = ViewBoundFeatureWrapper() + private val trackingProtectionOverlayObserver = ViewBoundFeatureWrapper() 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, id: Long?) { + override fun onCollectionCreated(title: String, sessions: List, id: Long?) { showTabSavedToCollectionSnackbar(sessions.size, true) } - override fun onTabsAdded(tabCollection: TabCollection, sessions: List) { + override fun onTabsAdded(tabCollection: TabCollection, sessions: List) { showTabSavedToCollectionSnackbar(sessions.size) } diff --git a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt index 0abb13a5d..1d129a027 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/OpenInAppOnboardingObserver.kt @@ -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) + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt index 8c4effc11..da19ec785 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/ToolbarGestureHandler.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt b/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt new file mode 100644 index 000000000..24576f932 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/browser/WebAuthnFeature.kt @@ -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 + } +} diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt index 0429f6b7c..a55e65957 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationController.kt @@ -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.toSessionBundle(sessionManager: SessionManager): List { - return this.mapNotNull { sessionManager.findSessionById(it.sessionId) } +fun List.toTabSessionStateList(store: BrowserStore): List { + 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, 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) { 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) ) } diff --git a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt index bb09b0d9c..012a73c88 100644 --- a/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/collections/CollectionCreationFragment.kt @@ -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 ) ) diff --git a/app/src/main/java/org/mozilla/fenix/components/Components.kt b/app/src/main/java/org/mozilla/fenix/components/Components.kt index ba77b9b93..73c8038a6 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Components.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Components.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/components/Core.kt b/app/src/main/java/org/mozilla/fenix/components/Core.kt index 1161ae7fa..96b6b504a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/Core.kt +++ b/app/src/main/java/org/mozilla/fenix/components/Core.kt @@ -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 ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt b/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt index dd92f560d..a2d35af89 100644 --- a/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt +++ b/app/src/main/java/org/mozilla/fenix/components/IntentProcessors.kt @@ -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 { diff --git a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt index 8535cc7fc..6a3f6940a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/PermissionStorage.kt @@ -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() } } diff --git a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt index 39da157c8..cbd15afa4 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TabCollectionStorage.kt @@ -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 = ObserverRegistry() ) : Observable by delegate { @@ -40,12 +37,12 @@ class TabCollectionStorage( /** * A collection has been created */ - fun onCollectionCreated(title: String, sessions: List, id: Long?) = Unit + fun onCollectionCreated(title: String, sessions: List, id: Long?) = Unit /** * Tab(s) have been added to collection */ - fun onTabsAdded(tabCollection: TabCollection, sessions: List) = Unit + fun onTabsAdded(tabCollection: TabCollection, sessions: List) = 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) = ioScope.launch { + suspend fun createCollection(title: String, sessions: List) = ioScope.launch { val id = collectionStorage.createCollection(title, sessions) notifyObservers { onCollectionCreated(title, sessions, id) } }.join() - suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List) = ioScope.launch { + suspend fun addTabsToCollection(tabCollection: TabCollection, sessions: List) = ioScope.launch { collectionStorage.addTabsToCollection(tabCollection, sessions) notifyObservers { onTabsAdded(tabCollection, sessions) } }.join() - fun getTabCollectionsCount(): Int { - return collectionStorage.getTabCollectionsCount() - } - fun getCollections(): LiveData> { return collectionStorage.getCollections().asLiveData() } - fun getCollectionsPaged(): DataSource.Factory { - return collectionStorage.getCollectionsPaged() - } - suspend fun removeCollection(tabCollection: TabCollection) = ioScope.launch { collectionStorage.removeCollection(tabCollection) }.join() diff --git a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt index bc614aef1..4dd0a4b00 100644 --- a/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt +++ b/app/src/main/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactory.kt @@ -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 ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt index 40250e646..956cda377 100644 --- a/app/src/main/java/org/mozilla/fenix/components/UseCases.kt +++ b/app/src/main/java/org/mozilla/fenix/components/UseCases.kt @@ -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. */ diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt index cc00144b0..cb475e7e5 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/Event.kt @@ -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? diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt index 19c5bd0c0..af9cb5a4a 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/GleanMetricsService.kt @@ -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( { TopSites.openDefault.record(it) } ) + is Event.TopSiteOpenGoogle -> EventWrapper( + { TopSites.openGoogleSearchAttribution.record(it) } + ) is Event.TopSiteOpenFrecent -> EventWrapper( { TopSites.openFrecency.record(it) } ) @@ -658,7 +663,13 @@ private val Event.wrapper: EventWrapper<*>? is Event.TabsTrayCloseAllTabsPressed -> EventWrapper( { TabsTray.closeAllTabs.record(it) } ) - Event.AutoPlaySettingVisited -> EventWrapper( + is Event.TabsTrayCfrDismissed -> EventWrapper( + { TabsTrayCfr.dismiss.record(it) } + ) + is Event.TabsTrayCfrTapped -> EventWrapper( + { TabsTrayCfr.goToSettings.record(it) } + ) + is Event.AutoPlaySettingVisited -> EventWrapper( { Autoplay.visitedSetting.record(it) } ) is Event.AutoPlaySettingChanged -> EventWrapper( @@ -691,15 +702,27 @@ private val Event.wrapper: EventWrapper<*>? { Events.recentlyClosedTabsOpened.record(it) } ) - Event.MasterPasswordMigrationDisplayed -> EventWrapper( + is Event.MasterPasswordMigrationDisplayed -> EventWrapper( { MasterPassword.displayed.record(it) } ) - Event.MasterPasswordMigrationSuccess -> EventWrapper( + is Event.MasterPasswordMigrationSuccess -> EventWrapper( { MasterPassword.migration.record(it) } ) - Event.TabSettingsOpened -> EventWrapper( + is Event.TabSettingsOpened -> EventWrapper( { Tabs.settingOpened.record(it) } ) + Event.ContextMenuCopyTapped -> EventWrapper( + { ContextualMenu.copyTapped.record(it) } + ) + is Event.ContextMenuSearchTapped -> EventWrapper( + { ContextualMenu.searchTapped.record(it) } + ) + is Event.ContextMenuSelectAllTapped -> EventWrapper( + { ContextualMenu.selectAllTapped.record(it) } + ) + is Event.ContextMenuShareTapped -> EventWrapper( + { ContextualMenu.shareTapped.record(it) } + ) // Don't record other events in Glean: is Event.AddBookmark -> null diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt index 6f2234948..3b31385c2 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricController.kt @@ -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" + } } diff --git a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt index 18b53961a..8857290c3 100644 --- a/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/components/metrics/MetricsUtils.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt index 079796a25..69c384865 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarController.kt @@ -77,7 +77,7 @@ class DefaultBrowserToolbarController( store.updateSearchTermsOfSelectedSession(text) activity.components.useCases.searchUseCases.defaultSearch.invoke( text, - sessionId = sessionManager.selectedSession?.id + sessionId = store.state.selectedTabId ) } diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt index a513c52c7..54f91e70f 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/BrowserToolbarView.kt @@ -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 ) diff --git a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt index b07f53636..018bf6efb 100644 --- a/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/components/toolbar/ToolbarIntegration.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterController.kt b/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterController.kt index fba8d2f39..4183a4bfd 100644 --- a/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterController.kt +++ b/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterController.kt @@ -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( diff --git a/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterFragment.kt b/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterFragment.kt index eff300e69..6f74525ea 100644 --- a/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/crashes/CrashReporterFragment.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt index 8fe71a71e..f82398c6a 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenu.kt @@ -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 { diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt index 475274472..04e902d51 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/CustomTabsIntegration.kt @@ -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, diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt index 864a3692c..689a6aa4c 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserActivity.kt @@ -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) } } } diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt index dc983ff12..016f220c0 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/ExternalAppBrowserFragment.kt @@ -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() @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() ) diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessor.kt index ee5c9f94f..1eee927a2 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessor.kt @@ -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 /mozilla//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 { diff --git a/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt b/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt index 7a79fe61f..9e4ce375c 100644 --- a/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt +++ b/app/src/main/java/org/mozilla/fenix/customtabs/PoweredByNotification.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/downloads/DownloadService.kt b/app/src/main/java/org/mozilla/fenix/downloads/DownloadService.kt index 4a7365ce7..25d894fb5 100644 --- a/app/src/main/java/org/mozilla/fenix/downloads/DownloadService.kt +++ b/app/src/main/java/org/mozilla/fenix/downloads/DownloadService.kt @@ -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) } } diff --git a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt index 1ceb0c2e6..1eb2cb27b 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Fragment.kt @@ -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]. diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index 3096f907c..49cf69cd5 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/NotificationsIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/NotificationsIntentProcessor.kt deleted file mode 100644 index 80a94d3d1..000000000 --- a/app/src/main/java/org/mozilla/fenix/home/intent/NotificationsIntentProcessor.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt index 4e91e5ef4..dec4cac01 100644 --- a/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt +++ b/app/src/main/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessor.kt @@ -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 } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt index e789a9b90..e8d22d4c2 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlAdapter.kt @@ -43,7 +43,12 @@ sealed class AdapterItem(@LayoutRes val viewType: Int) { ButtonTipViewHolder.LAYOUT_ID ) - data class TopSitePager(val topSites: List) : AdapterItem(TopSitePagerViewHolder.LAYOUT_ID) { + data class TopSitePagerPayload( + val changed: Set> + ) + + data class TopSitePager(val topSites: List) : + 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>() + 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 + ) { + 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) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt index a7c1ac97b..1fa618487 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/SessionControlController.kt @@ -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() diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt index 89b6ac0a9..36ed91467 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/TopSitePagerViewHolder.kt @@ -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) { val chunkedTopSites = topSites.chunked(TOP_SITES_PER_PAGE) topSitesPagerAdapter.submitList(chunkedTopSites) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt index 8934d9fee..668c17cee 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/onboarding/OnboardingPrivacyNoticeViewHolder.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt index d511081d7..78bf438d0 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapter.kt @@ -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(DiffCallback) { +) : ListAdapter(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() { - 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 + ) { + 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() { + 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 + } } } diff --git a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt index e1d1d571f..c960dc69f 100644 --- a/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapter.kt @@ -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, TopSiteViewHolder>(DiffCallback) { +) : ListAdapter, 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 + ) { + 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>() { + internal object TopSiteListDiffCallback : DiffUtil.ItemCallback>() { override fun areItemsTheSame(oldItem: List, newItem: List): Boolean { return oldItem.size == newItem.size } @@ -36,5 +55,15 @@ class TopSitesPagerAdapter( override fun areContentsTheSame(oldItem: List, newItem: List): Boolean { return newItem.zip(oldItem).all { (new, old) -> new == old } } + + override fun getChangePayload(oldItem: List, newItem: List): Any? { + val changed = mutableSetOf>() + 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 + } } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index 26ebe09eb..259d7ce11 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -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(), UserInteractionHandl ::deleteHistoryItems, ::syncHistory, requireComponents.analytics.metrics - ) + ) historyInteractor = HistoryInteractor( historyController ) @@ -271,6 +273,7 @@ class HistoryFragment : LibraryPageFragment(), 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(), UserInteractionHandl } } + private suspend fun deleteOpenTabsEngineHistory(store: BrowserStore) { + store.dispatch(EngineAction.PurgeHistoryAction).join() + } + private fun share(data: List) { requireComponents.analytics.metrics.track(Event.HistoryItemShared) val directions = HistoryFragmentDirections.actionGlobalShareFragment( diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt index 12ee4590b..67bd16500 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedAdapter.kt @@ -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(DiffCallback) { +) : ListAdapter(DiffCallback) { override fun onCreateViewHolder( parent: ViewGroup, viewType: Int @@ -26,11 +26,11 @@ class RecentlyClosedAdapter( holder.bind(getItem(position)) } - private object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ClosedTab, newItem: ClosedTab) = + private object DiffCallback : DiffUtil.ItemCallback() { + 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 } } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt index 0f71bf023..a4461a246 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedController.kt @@ -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 ) } } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt index 743e33c18..01e2188f6 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragment.kt @@ -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() { +class RecentlyClosedFragment : LibraryPageFragment() { private lateinit var recentlyClosedFragmentStore: RecentlyClosedFragmentStore private var _recentlyClosedFragmentView: RecentlyClosedFragmentView? = null protected val recentlyClosedFragmentView: RecentlyClosedFragmentView @@ -82,7 +82,7 @@ class RecentlyClosedFragment : LibraryPageFragment() { 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() { _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() { } } - override val selectedItems: Set = setOf() + override val selectedItems: Set = setOf() } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt index b62b430b2..a4ffed2dc 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractor.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt index cb75dabca..37b4d14c0 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentStore.kt @@ -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) : RecentlyClosedFragmentAction() + data class Change(val list: List) : RecentlyClosedFragmentAction() } /** * The state for the Recently Closed Screen * @property items List of recently closed tabs to display */ -data class RecentlyClosedFragmentState(val items: List = emptyList()) : State +data class RecentlyClosedFragmentState(val items: List = emptyList()) : State /** * The RecentlyClosedFragmentState Reducer. diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt index d6297c13b..740e82fa0 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentView.kt @@ -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) { + fun update(items: List) { recently_closed_empty_view.isVisible = items.isEmpty() recently_closed_list.isVisible = items.isNotEmpty() recentlyClosedAdapter.submitList(items) diff --git a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt index e60cc34ea..7d8e1f346 100644 --- a/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt +++ b/app/src/main/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedItemViewHolder.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt b/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt new file mode 100644 index 000000000..e34d7a8d0 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/perf/PerformanceInflater.kt @@ -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) +} diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt index f259b9b51..6e87cf2cc 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogController.kt @@ -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. * diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt index 8ef26cde8..6e20a2972 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogFragment.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt index cb7385ec7..9f82379d9 100644 --- a/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/SearchDialogInteractor.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt index e8f18cb7e..bb0c62a62 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarInteractor.kt @@ -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 */ diff --git a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt index 541711b35..7d97dea8f 100644 --- a/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt +++ b/app/src/main/java/org/mozilla/fenix/search/awesomebar/AwesomeBarView.kt @@ -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) { diff --git a/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetry.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetry.kt index 1be9e832c..f2ad23fea 100644 --- a/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetry.kt +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetry.kt @@ -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" } } diff --git a/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/TrackKeyInfo.kt b/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/TrackKeyInfo.kt index 73473943f..b67ec1522 100644 --- a/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/TrackKeyInfo.kt +++ b/app/src/main/java/org/mozilla/fenix/search/telemetry/incontent/TrackKeyInfo.kt @@ -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 "" } } diff --git a/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt b/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt index 843a638d3..94f21a60a 100644 --- a/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt +++ b/app/src/main/java/org/mozilla/fenix/session/PrivateNotificationService.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/Extensions.kt b/app/src/main/java/org/mozilla/fenix/settings/Extensions.kt index 2ede851aa..837249068 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/Extensions.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/Extensions.kt @@ -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) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt index 46e9d13e1..e46b6ae52 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/PhoneFeature.kt @@ -29,6 +29,7 @@ enum class PhoneFeature(val androidPermissionsList: Array) : 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) : 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) : 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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt index 9a8758aca..2bf9ad3a0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SettingsFragment.kt @@ -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(R.string.pref_key_private_browsing) - val preferenceExternalDownloadManager = - requirePreference(R.string.pref_key_external_download_manager) val preferenceLeakCanary = findPreference(leakKey) val preferenceRemoteDebugging = findPreference(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 { 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 =N -> Navigate user to Android Default Apps Settings. + * For 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(R.string.pref_key_make_default_browser).updateSwitch() } diff --git a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt index c507489cc..03b873a99 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/SupportUtils.kt @@ -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"), diff --git a/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt index 029183ac5..66bb347cc 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/TrackingProtectionFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/AuthIntentReceiverActivity.kt b/app/src/main/java/org/mozilla/fenix/settings/account/AuthIntentReceiverActivity.kt index 427b81d9e..fde2c14a8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/AuthIntentReceiverActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/AuthIntentReceiverActivity.kt @@ -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) diff --git a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt index d927485dd..fe0b30c2a 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/account/SignOutFragment.kt @@ -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 diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt index 8550cd650..e5fe0ac42 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteAndQuit.kt @@ -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() } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt index 7b02cd526..6d28fe7d3 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataController.kt @@ -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() + } + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt index 95732ce21..4bb7dcd1c 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataFragment.kt @@ -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 } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataOnQuitType.kt b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataOnQuitType.kt index 794d16a9d..a88c4f2bb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataOnQuitType.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/deletebrowsingdata/DeleteBrowsingDataOnQuitType.kt @@ -14,7 +14,8 @@ enum class DeleteBrowsingDataOnQuitType(@StringRes private val prefKey: Int) { HISTORY(R.string.pref_key_delete_browsing_history_on_quit), COOKIES(R.string.pref_key_delete_cookies_on_quit), CACHE(R.string.pref_key_delete_caches_on_quit), - PERMISSIONS(R.string.pref_key_delete_permissions_on_quit); + PERMISSIONS(R.string.pref_key_delete_permissions_on_quit), + DOWNLOADS(R.string.pref_key_delete_downloads_on_quit); fun getPreferenceKey(context: Context) = context.getPreferenceKey(prefKey) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt index b20200fbf..570d92392 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/logins/fragment/SavedLoginsAuthFragment.kt @@ -24,13 +24,13 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import mozilla.components.support.base.feature.ViewBoundFeatureWrapper import org.mozilla.fenix.R -import org.mozilla.fenix.addons.runIfFragmentIsAttached import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.secure import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.ext.runIfFragmentIsAttached import org.mozilla.fenix.settings.SharedPreferenceUpdater import org.mozilla.fenix.settings.logins.biometric.BiometricPromptFeature import org.mozilla.fenix.settings.logins.SyncLoginsPreferenceView diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt index aa6211c8b..f93b9c69f 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsController.kt @@ -6,10 +6,13 @@ package org.mozilla.fenix.settings.quicksettings import android.content.Context import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri import androidx.navigation.NavController import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import mozilla.components.browser.session.Session +import mozilla.components.browser.state.selector.findTabOrCustomTab +import mozilla.components.browser.state.store.BrowserStore import mozilla.components.feature.session.SessionUseCases.ReloadUrlUseCase import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.feature.tabs.TabsUseCases.AddNewTabUseCase @@ -39,6 +42,13 @@ interface QuickSettingsController { */ fun handlePermissionToggled(permission: WebsitePermission) + /** + * Handles change a [WebsitePermission.Autoplay]. + * + * @param autoplayValue [AutoplayValue] needing to be changed. + */ + fun handleAutoplayChanged(autoplayValue: AutoplayValue) + /** * Handles a certain set of Android permissions being explicitly granted by the user. * @@ -70,10 +80,13 @@ interface QuickSettingsController { class DefaultQuickSettingsController( private val context: Context, private val quickSettingsStore: QuickSettingsFragmentStore, + private val browserStore: BrowserStore, private val ioScope: CoroutineScope, private val navController: NavController, - private val session: Session?, - private var sitePermissions: SitePermissions?, + @VisibleForTesting + internal val sessionId: String, + @VisibleForTesting + internal var sitePermissions: SitePermissions?, private val settings: Settings, private val permissionStorage: PermissionStorage, private val reload: ReloadUrlUseCase, @@ -122,6 +135,28 @@ class DefaultQuickSettingsController( ) } + override fun handleAutoplayChanged(autoplayValue: AutoplayValue) { + val permissions = sitePermissions + + sitePermissions = if (permissions == null) { + val tab = browserStore.state.findTabOrCustomTab(sessionId) + val origin = requireNotNull(tab?.content?.url?.toUri()?.host) { + "An origin is required to change a autoplay settings from the door hanger" + } + val sitePermissions = + autoplayValue.createSitePermissionsFromCustomRules(origin, settings) + handleAutoplayAdd(sitePermissions) + sitePermissions + } else { + val newPermission = autoplayValue.updateSitePermissions(permissions) + handlePermissionsChange(autoplayValue.updateSitePermissions(newPermission)) + newPermission + } + quickSettingsStore.dispatch( + WebsitePermissionAction.ChangeAutoplay(autoplayValue) + ) + } + /** * Request a certain set of runtime Android permissions. * @@ -144,7 +179,15 @@ class DefaultQuickSettingsController( fun handlePermissionsChange(updatedPermissions: SitePermissions) { ioScope.launch { permissionStorage.updateSitePermissions(updatedPermissions) - reload(session) + reload(sessionId) + } + } + + @VisibleForTesting + internal fun handleAutoplayAdd(sitePermissions: SitePermissions) { + ioScope.launch { + permissionStorage.add(sitePermissions) + reload(sessionId) } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt index ad1eae90b..48d785ca2 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentAction.kt @@ -20,7 +20,7 @@ sealed class WebsiteInfoAction : QuickSettingsFragmentAction() /** * All possible [WebsitePermissionsState] changes as result of user / system interactions. */ -sealed class WebsitePermissionAction : QuickSettingsFragmentAction() { +sealed class WebsitePermissionAction(open val updatedFeature: PhoneFeature) : QuickSettingsFragmentAction() { /** * Change resulting from toggling a specific [WebsitePermission] for the current website. * @@ -31,8 +31,18 @@ sealed class WebsitePermissionAction : QuickSettingsFragmentAction() { * @param updatedEnabledStatus [Boolean] the new [WebsitePermission#enabled] which will be shown to the user. */ class TogglePermission( - val updatedFeature: PhoneFeature, + override val updatedFeature: PhoneFeature, val updatedStatus: String, val updatedEnabledStatus: Boolean - ) : WebsitePermissionAction() + ) : WebsitePermissionAction(updatedFeature) + + /** + * Change resulting from changing a specific [WebsitePermission.Autoplay] for the current website. + * + * @param autoplayValue [AutoplayValue] backing a certain [WebsitePermission.Autoplay]. + * Allows to easily identify which permission changed + */ + class ChangeAutoplay( + val autoplayValue: AutoplayValue + ) : WebsitePermissionAction(PhoneFeature.AUTOPLAY) } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt index 5f15579b1..e26c7f3f0 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducer.kt @@ -35,16 +35,26 @@ object WebsitePermissionsStateReducer { state: WebsitePermissionsState, action: WebsitePermissionAction ): WebsitePermissionsState { + val key = action.updatedFeature + val value = state.getValue(key) + return when (action) { is WebsitePermissionAction.TogglePermission -> { - val key = action.updatedFeature - val newWebsitePermission = state.getValue(key).copy( + val toggleable = value as WebsitePermission.Toggleable + val newWebsitePermission = toggleable.copy( status = action.updatedStatus, isEnabled = action.updatedEnabledStatus ) state + Pair(key, newWebsitePermission) } + is WebsitePermissionAction.ChangeAutoplay -> { + val autoplay = value as WebsitePermission.Autoplay + val newWebsitePermission = autoplay.copy( + autoplayValue = action.autoplayValue + ) + state + Pair(key, newWebsitePermission) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt index ecb2ef80c..056db2e20 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentState.kt @@ -4,12 +4,18 @@ package org.mozilla.fenix.settings.quicksettings +import android.content.Context import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import mozilla.components.feature.sitepermissions.SitePermissions +import mozilla.components.feature.sitepermissions.SitePermissions.AutoplayStatus +import mozilla.components.feature.sitepermissions.SitePermissionsRules +import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction import mozilla.components.lib.state.State import org.mozilla.fenix.R import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.utils.Settings /** * [State] containing all data displayed to the user by this Fragment. @@ -70,10 +76,192 @@ typealias WebsitePermissionsState = Map * @property isBlockedByAndroid Whether the corresponding *dangerous* Android permission is granted * for the app by the user or not. */ -data class WebsitePermission( - val phoneFeature: PhoneFeature, - val status: String, - val isVisible: Boolean, - val isEnabled: Boolean, - val isBlockedByAndroid: Boolean -) +sealed class WebsitePermission( + open val phoneFeature: PhoneFeature, + open val status: String, + open val isVisible: Boolean, + open val isEnabled: Boolean, + open val isBlockedByAndroid: Boolean +) { + data class Autoplay( + val autoplayValue: AutoplayValue, + val options: List, + override val isVisible: Boolean + ) : WebsitePermission( + PhoneFeature.AUTOPLAY, + autoplayValue.label, + isVisible, + autoplayValue.isEnabled, + isBlockedByAndroid = false + ) + + data class Toggleable( + override val phoneFeature: PhoneFeature, + override val status: String, + override val isVisible: Boolean, + override val isEnabled: Boolean, + override val isBlockedByAndroid: Boolean + ) : WebsitePermission( + phoneFeature, + status, + isVisible, + isEnabled, + isBlockedByAndroid + ) +} + +sealed class AutoplayValue( + open val label: String, + open val rules: SitePermissionsRules, + open val sitePermission: SitePermissions? +) { + override fun toString() = label + abstract fun isSelected(): Boolean + abstract fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions + abstract fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions + abstract val isEnabled: Boolean + + val isVisible: Boolean get() = isSelected() + + data class AllowAll( + override val label: String, + override val rules: SitePermissionsRules, + override val sitePermission: SitePermissions? + ) : AutoplayValue(label, rules, sitePermission) { + override val isEnabled: Boolean = true + override fun toString() = super.toString() + override fun isSelected(): Boolean { + val actions = if (sitePermission !== null) { + listOf( + sitePermission.autoplayAudible, + sitePermission.autoplayInaudible + ) + } else { + listOf(rules.autoplayAudible.toAutoplayStatus(), rules.autoplayInaudible.toAutoplayStatus()) + } + + return actions.all { it == AutoplayStatus.ALLOWED } + } + + override fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions { + val rules = settings.getSitePermissionsCustomSettingsRules() + return rules.copy( + autoplayAudible = AutoplayAction.ALLOWED, + autoplayInaudible = AutoplayAction.ALLOWED + ).toSitePermissions(origin) + } + + override fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions { + return sitePermissions.copy( + autoplayAudible = AutoplayStatus.ALLOWED, + autoplayInaudible = AutoplayStatus.ALLOWED + ) + } + } + + data class BlockAll( + override val label: String, + override val rules: SitePermissionsRules, + override val sitePermission: SitePermissions? + ) : AutoplayValue(label, rules, sitePermission) { + override val isEnabled: Boolean = false + override fun toString() = super.toString() + override fun isSelected(): Boolean { + val actions = if (sitePermission !== null) { + listOf( + sitePermission.autoplayAudible, + sitePermission.autoplayInaudible + ) + } else { + listOf(rules.autoplayAudible.toAutoplayStatus(), rules.autoplayInaudible.toAutoplayStatus()) + } + + return actions.all { it == AutoplayStatus.BLOCKED } + } + override fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions { + val rules = settings.getSitePermissionsCustomSettingsRules() + return rules.copy( + autoplayAudible = AutoplayAction.BLOCKED, + autoplayInaudible = AutoplayAction.BLOCKED + ).toSitePermissions(origin) + } + + override fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions { + return sitePermissions.copy( + autoplayAudible = AutoplayStatus.BLOCKED, + autoplayInaudible = AutoplayStatus.BLOCKED + ) + } + } + + data class BlockAudible( + override val label: String, + override val rules: SitePermissionsRules, + override val sitePermission: SitePermissions? + ) : AutoplayValue(label, rules, sitePermission) { + override val isEnabled: Boolean = false + override fun toString() = super.toString() + override fun isSelected(): Boolean { + val (audible, inaudible) = if (sitePermission !== null) { + sitePermission.autoplayAudible to sitePermission.autoplayInaudible + } else { + rules.autoplayAudible.toAutoplayStatus() to rules.autoplayInaudible.toAutoplayStatus() + } + + return audible == AutoplayStatus.BLOCKED && inaudible == AutoplayStatus.ALLOWED + } + + override fun createSitePermissionsFromCustomRules(origin: String, settings: Settings): SitePermissions { + val rules = settings.getSitePermissionsCustomSettingsRules() + return rules.copy(autoplayAudible = AutoplayAction.BLOCKED, autoplayInaudible = AutoplayAction.ALLOWED) + .toSitePermissions(origin) + } + + override fun updateSitePermissions(sitePermissions: SitePermissions): SitePermissions { + return sitePermissions.copy( + autoplayInaudible = AutoplayStatus.ALLOWED, + autoplayAudible = AutoplayStatus.BLOCKED + ) + } + } + + companion object { + fun values( + context: Context, + settings: Settings, + sitePermission: SitePermissions? + ): List { + val rules = settings.getSitePermissionsCustomSettingsRules() + return listOf( + AllowAll( + context.getString(R.string.preference_option_autoplay_allowed2), + rules, + sitePermission + ), + BlockAll( + context.getString(R.string.preference_option_autoplay_blocked3), + rules, + sitePermission + ), + BlockAudible( + context.getString(R.string.preference_option_autoplay_block_audio2), + rules, + sitePermission + ) + ) + } + + fun getFallbackValue( + context: Context, + settings: Settings, + sitePermission: SitePermissions? + ): AutoplayValue { + val rules = settings.getSitePermissionsCustomSettingsRules() + return BlockAudible( + context.getString(R.string.preference_option_autoplay_block_audio2), + rules, + sitePermission + ) + } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt index e5d0000dc..ba4efd1aa 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStore.kt @@ -6,6 +6,7 @@ package org.mozilla.fenix.settings.quicksettings import android.content.Context import androidx.annotation.VisibleForTesting +import mozilla.components.browser.state.state.content.PermissionHighlightsState import mozilla.components.feature.sitepermissions.SitePermissions import mozilla.components.lib.state.Action import mozilla.components.lib.state.Reducer @@ -57,13 +58,20 @@ class QuickSettingsFragmentStore( certificateName: String, isSecured: Boolean, permissions: SitePermissions?, + permissionHighlights: PermissionHighlightsState, settings: Settings ) = QuickSettingsFragmentStore( QuickSettingsFragmentState( - webInfoState = createWebsiteInfoState(websiteUrl, websiteTitle, isSecured, certificateName), + webInfoState = createWebsiteInfoState( + websiteUrl, + websiteTitle, + isSecured, + certificateName + ), websitePermissionsState = createWebsitePermissionState( context, permissions, + permissionHighlights, settings ) ) @@ -90,7 +98,8 @@ class QuickSettingsFragmentStore( } /** - * Construct an initial [WebsitePermissionsState] to be rendered by [WebsitePermissionsView] + * Construct an initial [WebsitePermissions + * State] to be rendered by [WebsitePermissionsView] * containing the permissions requested by the current website. * * Users can modify the returned [WebsitePermissionsState] after it is initially displayed. @@ -103,11 +112,17 @@ class QuickSettingsFragmentStore( fun createWebsitePermissionState( context: Context, permissions: SitePermissions?, + permissionHighlights: PermissionHighlightsState, settings: Settings ): WebsitePermissionsState { val state = EnumMap(PhoneFeature::class.java) for (feature in PhoneFeature.values()) { - state[feature] = feature.toWebsitePermission(context, permissions, settings) + state[feature] = feature.toWebsitePermission( + context, + permissions, + permissionHighlights, + settings + ) } return state } @@ -119,15 +134,31 @@ class QuickSettingsFragmentStore( fun PhoneFeature.toWebsitePermission( context: Context, permissions: SitePermissions?, + permissionHighlights: PermissionHighlightsState, settings: Settings ): WebsitePermission { - return WebsitePermission( - phoneFeature = this, - status = getActionLabel(context, permissions, settings), - isVisible = shouldBeVisible(permissions, settings), - isEnabled = shouldBeEnabled(context, permissions, settings), - isBlockedByAndroid = !isAndroidPermissionGranted(context) - ) + return if (this == PhoneFeature.AUTOPLAY) { + val autoplayValues = AutoplayValue.values(context, settings, permissions) + val selected = + autoplayValues.firstOrNull { it.isSelected() } ?: AutoplayValue.getFallbackValue( + context, + settings, + permissions + ) + WebsitePermission.Autoplay( + autoplayValue = selected, + options = autoplayValues, + isVisible = permissionHighlights.isAutoPlayBlocking || permissions !== null + ) + } else { + WebsitePermission.Toggleable( + phoneFeature = this, + status = getActionLabel(context, permissions, settings), + isVisible = shouldBeVisible(permissions, settings), + isEnabled = shouldBeEnabled(context, permissions, settings), + isBlockedByAndroid = !isAndroidPermissionGranted(context) + ) + } } } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt index 6805c3e38..0b5ea9086 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractor.kt @@ -23,4 +23,8 @@ class QuickSettingsInteractor( override fun onPermissionToggled(permissionState: WebsitePermission) { controller.handlePermissionToggled(permissionState) } + + override fun onAutoplayChanged(value: AutoplayValue) { + controller.handleAutoplayChanged(value) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt index d38591b87..e8047d24b 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsSheetDialogFragment.kt @@ -63,7 +63,6 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() { val context = requireContext() val components = context.components val rootView = inflateRootView(container) - quickSettingsStore = QuickSettingsFragmentStore.createStore( context = context, websiteUrl = args.url, @@ -71,15 +70,17 @@ class QuickSettingsSheetDialogFragment : AppCompatDialogFragment() { isSecured = args.isSecured, permissions = args.sitePermissions, settings = components.settings, - certificateName = args.certificateName + certificateName = args.certificateName, + permissionHighlights = args.permissionHighlights ) quickSettingsController = DefaultQuickSettingsController( context = context, quickSettingsStore = quickSettingsStore, + browserStore = components.core.store, ioScope = viewLifecycleOwner.lifecycleScope + Dispatchers.IO, navController = findNavController(), - session = components.core.sessionManager.findSessionById(args.sessionId), + sessionId = args.sessionId, sitePermissions = args.sitePermissions, settings = components.settings, permissionStorage = components.core.permissionStorage, diff --git a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt index fbfb00e03..8cc26d7fb 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionsView.kt @@ -7,12 +7,25 @@ package org.mozilla.fenix.settings.quicksettings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.widget.AppCompatSpinner import androidx.core.view.isVisible import kotlinx.android.extensions.LayoutContainer import kotlinx.android.synthetic.main.quicksettings_permissions.view.* import org.mozilla.fenix.R import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY +import org.mozilla.fenix.settings.PhoneFeature.CAMERA +import org.mozilla.fenix.settings.PhoneFeature.MICROPHONE +import org.mozilla.fenix.settings.PhoneFeature.LOCATION +import org.mozilla.fenix.settings.PhoneFeature.NOTIFICATION +import org.mozilla.fenix.settings.PhoneFeature.PERSISTENT_STORAGE +import org.mozilla.fenix.settings.PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS +import org.mozilla.fenix.settings.quicksettings.WebsitePermissionsView.PermissionViewHolder.SpinnerPermission +import org.mozilla.fenix.settings.quicksettings.WebsitePermissionsView.PermissionViewHolder.ToggleablePermission import java.util.EnumMap /** @@ -31,6 +44,13 @@ interface WebsitePermissionInteractor { * @param permissionState current [WebsitePermission] that the user wants toggled. */ fun onPermissionToggled(permissionState: WebsitePermission) + + /** + * Indicates the user changed the status of a an autoplay permission. + * + * @param value current [AutoplayValue] that the user wants change. + */ + fun onAutoplayChanged(value: AutoplayValue) } /** @@ -52,25 +72,30 @@ class WebsitePermissionsView( val view: View = LayoutInflater.from(context) .inflate(R.layout.quicksettings_permissions, containerView, true) - private val permissionViews: Map = EnumMap( + @VisibleForTesting + internal var permissionViews: Map = EnumMap( mapOf( - PhoneFeature.CAMERA to PermissionViewHolder(view.cameraLabel, view.cameraStatus), - PhoneFeature.LOCATION to PermissionViewHolder(view.locationLabel, view.locationStatus), - PhoneFeature.MICROPHONE to PermissionViewHolder( + CAMERA to ToggleablePermission(view.cameraLabel, view.cameraStatus), + LOCATION to ToggleablePermission(view.locationLabel, view.locationStatus), + MICROPHONE to ToggleablePermission( view.microphoneLabel, view.microphoneStatus ), - PhoneFeature.NOTIFICATION to PermissionViewHolder( + NOTIFICATION to ToggleablePermission( view.notificationLabel, view.notificationStatus ), - PhoneFeature.PERSISTENT_STORAGE to PermissionViewHolder( + PERSISTENT_STORAGE to ToggleablePermission( view.persistentStorageLabel, view.persistentStorageStatus ), - PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS to PermissionViewHolder( + MEDIA_KEY_SYSTEM_ACCESS to ToggleablePermission( view.mediaKeySystemAccessLabel, view.mediaKeySystemAccessStatus + ), + AUTOPLAY to SpinnerPermission( + view.autoplayLabel, + view.autoplayStatus ) ) ) @@ -102,13 +127,77 @@ class WebsitePermissionsView( * @param permissionState [WebsitePermission] specific permission that can be shown to the user. * @param viewHolder Views that will render [WebsitePermission]'s state. */ - private fun bindPermission(permissionState: WebsitePermission, viewHolder: PermissionViewHolder) { + @VisibleForTesting + internal fun bindPermission( + permissionState: WebsitePermission, + viewHolder: PermissionViewHolder + ) { viewHolder.label.isEnabled = permissionState.isEnabled viewHolder.label.isVisible = permissionState.isVisible - viewHolder.status.text = permissionState.status viewHolder.status.isVisible = permissionState.isVisible - viewHolder.status.setOnClickListener { interactor.onPermissionToggled(permissionState) } + + when (viewHolder) { + is ToggleablePermission -> { + viewHolder.status.text = permissionState.status + viewHolder.status.setOnClickListener { + interactor.onPermissionToggled( + permissionState + ) + } + } + is SpinnerPermission -> { + if (permissionState !is WebsitePermission.Autoplay) { + throw IllegalArgumentException("${permissionState.phoneFeature} is not supported") + } + + val selectedIndex = permissionState.options.indexOf(permissionState.autoplayValue) + val adapter = ArrayAdapter( + context, + R.layout.quicksettings_permission_spinner_item, + permissionState.options + ) + adapter.setDropDownViewResource(R.layout.quicksetting_permission_spinner_dropdown) + viewHolder.status.adapter = adapter + + viewHolder.status.tag = permissionState.autoplayValue + viewHolder.status.setSelection(selectedIndex) + viewHolder.status.onItemSelectedListener = + object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + // Unfortunately the spinner component triggers an selection event when initialized, + // to avoid that, we are using the tag property to store the selected value and + // be able to differentiate from an initialization event from a normal selection event + // see https://stackoverflow.com/questions/21747917/undesired-onitemselected-calls/21751327#21751327 + if (viewHolder.status.selectedItem == viewHolder.status.tag) { + return + } + viewHolder.status.tag = viewHolder.status.selectedItem + val type = viewHolder.status.selectedItem as AutoplayValue + interactor.onAutoplayChanged(type) + } + + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + } + } } - data class PermissionViewHolder(val label: TextView, val status: TextView) + sealed class PermissionViewHolder(open val label: TextView, open val status: View) { + data class ToggleablePermission( + override val label: TextView, + override val status: TextView + ) : + PermissionViewHolder(label, status) + + data class SpinnerPermission( + override val label: TextView, + override val status: AppCompatSpinner + ) : + PermissionViewHolder(label, status) + } } diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt index 5b4d0aa99..399d46ca8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragment.kt @@ -4,8 +4,10 @@ package org.mozilla.fenix.settings.sitepermissions +import android.content.Context import android.content.DialogInterface import android.os.Bundle +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope import androidx.navigation.findNavController @@ -19,6 +21,7 @@ import mozilla.components.feature.sitepermissions.SitePermissions import org.mozilla.fenix.R import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar import org.mozilla.fenix.settings.PhoneFeature import org.mozilla.fenix.settings.PhoneFeature.CAMERA @@ -27,10 +30,15 @@ import org.mozilla.fenix.settings.PhoneFeature.MICROPHONE import org.mozilla.fenix.settings.PhoneFeature.NOTIFICATION import org.mozilla.fenix.settings.PhoneFeature.PERSISTENT_STORAGE import org.mozilla.fenix.settings.PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS +import org.mozilla.fenix.settings.PhoneFeature.AUTOPLAY +import org.mozilla.fenix.settings.quicksettings.AutoplayValue import org.mozilla.fenix.settings.requirePreference +import org.mozilla.fenix.utils.Settings +@SuppressWarnings("TooManyFunctions") class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { - private lateinit var sitePermissions: SitePermissions + @VisibleForTesting + internal lateinit var sitePermissions: SitePermissions override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,19 +62,22 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { } } - private fun bindCategoryPhoneFeatures() { + @VisibleForTesting + internal fun bindCategoryPhoneFeatures() { initPhoneFeature(CAMERA) initPhoneFeature(LOCATION) initPhoneFeature(MICROPHONE) initPhoneFeature(NOTIFICATION) initPhoneFeature(PERSISTENT_STORAGE) initPhoneFeature(MEDIA_KEY_SYSTEM_ACCESS) + initAutoplayFeature() bindClearPermissionsButton() } - private fun initPhoneFeature(phoneFeature: PhoneFeature) { - val summary = phoneFeature.getActionLabel(requireContext(), sitePermissions) - val cameraPhoneFeatures = requirePreference(phoneFeature.getPreferenceId()) + @VisibleForTesting + internal fun initPhoneFeature(phoneFeature: PhoneFeature) { + val summary = phoneFeature.getActionLabel(provideContext(), sitePermissions) + val cameraPhoneFeatures = getPreference(phoneFeature) cameraPhoneFeatures.summary = summary cameraPhoneFeatures.onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -75,7 +86,44 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { } } - private fun bindClearPermissionsButton() { + @VisibleForTesting + internal fun getPreference(phoneFeature: PhoneFeature): Preference = + requirePreference(phoneFeature.getPreferenceId()) + + @VisibleForTesting + internal fun provideContext(): Context = requireContext() + + @VisibleForTesting + internal fun provideSettings(): Settings = provideContext().settings() + + @VisibleForTesting + internal fun initAutoplayFeature() { + val phoneFeature = getPreference(AUTOPLAY) + phoneFeature.summary = getAutoplayLabel() + + phoneFeature.onPreferenceClickListener = Preference.OnPreferenceClickListener { + navigateToPhoneFeature(AUTOPLAY) + true + } + } + + @VisibleForTesting + internal fun getAutoplayLabel(): String { + val context = provideContext() + val settings = provideSettings() + val autoplayValues = AutoplayValue.values(context, settings, sitePermissions) + val selected = + autoplayValues.firstOrNull { it.isSelected() } ?: AutoplayValue.getFallbackValue( + context, + settings, + sitePermissions + ) + + return selected.label + } + + @VisibleForTesting + internal fun bindClearPermissionsButton() { val button: Preference = requirePreference(R.string.pref_key_exceptions_clear_site_permissions) button.onPreferenceClickListener = Preference.OnPreferenceClickListener { @@ -106,7 +154,8 @@ class SitePermissionsDetailsExceptionsFragment : PreferenceFragmentCompat() { } } - private fun navigateToPhoneFeature(phoneFeature: PhoneFeature) { + @VisibleForTesting + internal fun navigateToPhoneFeature(phoneFeature: PhoneFeature) { val directions = SitePermissionsDetailsExceptionsFragmentDirections.actionSitePermissionsToExceptionsToManagePhoneFeature( phoneFeature = phoneFeature, diff --git a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt index 110fc2bda..dab4a76e8 100644 --- a/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragment.kt @@ -14,7 +14,10 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.RadioButton +import androidx.annotation.IdRes +import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs @@ -27,8 +30,11 @@ import org.mozilla.fenix.R import org.mozilla.fenix.ext.requireComponents import org.mozilla.fenix.ext.settings import org.mozilla.fenix.ext.showToolbar +import org.mozilla.fenix.settings.PhoneFeature +import org.mozilla.fenix.settings.quicksettings.AutoplayValue import org.mozilla.fenix.settings.setStartCheckedIndicator import org.mozilla.fenix.settings.update +import org.mozilla.fenix.utils.Settings @SuppressWarnings("TooManyFunctions") class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { @@ -36,13 +42,14 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { private lateinit var radioAllow: RadioButton private lateinit var radioBlock: RadioButton private lateinit var blockedByAndroidView: View + @VisibleForTesting + internal lateinit var rootView: View private val args by navArgs() - val settings by lazy { requireContext().settings() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - showToolbar(args.phoneFeature.getLabel(requireContext())) + showToolbar(getFeature().getLabel(requireContext())) } override fun onCreateView( @@ -50,20 +57,62 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val rootView = + rootView = inflater.inflate(R.layout.fragment_manage_site_permissions_exceptions_feature_phone, container, false) - initAskToAllowRadio(rootView) - initBlockRadio(rootView) - bindBlockedByAndroidContainer(rootView) - initClearPermissionsButton(rootView) + if (getFeature() == PhoneFeature.AUTOPLAY) { + initAutoplay(getSitePermission()) + } else { + initNormalFeature() + } + bindBlockedByAndroidContainer() + initClearPermissionsButton() return rootView } + @VisibleForTesting + internal fun getFeature(): PhoneFeature = args.phoneFeature + + @VisibleForTesting + internal fun getSitePermission(): SitePermissions = args.sitePermissions + + @VisibleForTesting + internal fun getSettings(): Settings = requireContext().settings() + + fun initAutoplay(sitePermissions: SitePermissions? = null) { + val context = requireContext() + val autoplayValues = AutoplayValue.values(context, getSettings(), sitePermissions) + val allowAudioAndVideo = + requireNotNull(autoplayValues.find { it is AutoplayValue.AllowAll }) + val blockAll = requireNotNull(autoplayValues.find { it is AutoplayValue.BlockAll }) + val blockAudible = requireNotNull(autoplayValues.find { it is AutoplayValue.BlockAudible }) + + initAutoplayOption(R.id.ask_to_allow_radio, allowAudioAndVideo) + initAutoplayOption(R.id.block_radio, blockAll) + initAutoplayOption(R.id.optional_radio, blockAudible) + } + + fun initNormalFeature() { + initAskToAllowRadio(rootView) + initBlockRadio() + } + override fun onResume() { super.onResume() - initBlockedByAndroidView(args.phoneFeature, blockedByAndroidView) + initBlockedByAndroidView(getFeature(), blockedByAndroidView) + } + + @VisibleForTesting + internal fun initAutoplayOption(@IdRes viewId: Int, value: AutoplayValue) { + val radio = rootView.findViewById(viewId) + radio.isVisible = true + radio.text = value.label + + radio.setOnClickListener { + updatedSitePermissions(value) + } + radio.restoreState(value) } private fun initAskToAllowRadio(rootView: View) { @@ -79,13 +128,22 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { } private fun RadioButton.restoreState(status: SitePermissions.Status) { - if (args.phoneFeature.getStatus(args.sitePermissions) == status) { + val permissionsStatus = getFeature().getStatus(getSitePermission()) + if (permissionsStatus != SitePermissions.Status.NO_DECISION && permissionsStatus == status) { this.isChecked = true this.setStartCheckedIndicator() } } - private fun initBlockRadio(rootView: View) { + @VisibleForTesting + internal fun RadioButton.restoreState(autoplayValue: AutoplayValue) { + if (autoplayValue.isSelected()) { + this.isChecked = true + this.setStartCheckedIndicator() + } + } + + private fun initBlockRadio() { radioBlock = rootView.findViewById(R.id.block_radio) radioBlock.setOnClickListener { updatedSitePermissions(BLOCKED) @@ -93,7 +151,8 @@ class SitePermissionsManageExceptionsPhoneFeatureFragment : Fragment() { radioBlock.restoreState(BLOCKED) } - private fun initClearPermissionsButton(rootView: View) { + @VisibleForTesting + internal fun initClearPermissionsButton() { val button = rootView.findViewById