- 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أزِل اللسان من المجموعة
+
+ اختر ألسنةأغلِق اللسان
@@ -1012,6 +1014,8 @@
أُغلقت الألسنة!حُفظت العلامات!
+
+ اعرضأُضيف إلى المواقع الشائعة!
@@ -1088,6 +1092,8 @@
يحرّر مساحة التخزينتصاريح المواقع
+
+ التنزيلاتاحذف بيانات التصفح
@@ -1207,7 +1213,7 @@
خصوصيتك
- صمّمنا لك %s لتقدر على التحكّم فيما تُشاركه عبر الشبكة، وما تُشاركه معنا.
+ صمّمنا لك %s لتقدر على التحكّم فيما تُشاركه عبر الشبكة، وما تُشاركه معنا.اقرأ تنويه الخصوصية
@@ -1357,6 +1363,11 @@
The first parameter is the app name -->
%s | المكتبات مفتوحة المصدر
+
+ متعقّبات التحويلات
+
+ يمسح الكعكات التي ضبطتها التحويلات إلى مواقع تعقّب معروفة.
+
الدعم
@@ -1612,6 +1623,12 @@
احذفخيارات الولوج
+
+ حقل نصوص قابل للتعديل لعنوان وِب الولوج.
+
+ حقل نصوص قابل للتعديل لاسم مستخدم الولوج.
+
+ حقل نصوص قابل للتعديل لكلمة سر الولوج.احفظ التغييرات على الولوج.
@@ -1657,6 +1674,8 @@
الاسم
+
+ اسم الموقع الأكثر زيارةحسنا
diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml
index 374b740ea..d13fff47d 100644
--- a/app/src/main/res/values-ast/strings.xml
+++ b/app/src/main/res/values-ast/strings.xml
@@ -30,7 +30,7 @@
%1$s llingüetes abiertes. Toca pa cambiar a otra.
- %1$d esbillaes
+ %1$d na esbillaColeición nueva
@@ -44,13 +44,16 @@
Guardar les llingüetes esbillaes nuna coleición
- Esbillóse %1$s
+ %1$s na esbilla
- Deseleicionóse %1$s
+ %1$s fuera de la esbillaColesti del mou d\'esbilla múltiple
- Entresti nel mou d\'esbilla múltiple, esbilla les llingüetes pa guardar nuna coleición
+ Entresti nel mou d\'esbilla múltiple, esbilla les llingüetes pa guardales nuna coleición
+
+
+ Na esbilla%1$s ta producíu por Mozilla.
@@ -110,7 +113,7 @@
Llingüeta privada nueva
- Sitios destacaos
+ Sitios principales
@@ -178,7 +181,7 @@
- Nun pue conectase porque nun se reconoz l\'esquema de la URL.
+ Nun ye posible conectase porque nun se reconoz l\'esquema de la URL.
@@ -301,7 +304,7 @@
Cuenta de Firefox
- Reconéutate pa siguir cola sincronización
+ Volvi conectate pa siguir cola sincronizaciónLlingua
@@ -454,7 +457,7 @@
Aniciar sesión pa reconectar
- Desaniciar la cuenta
+ Quitar la cuenta
@@ -483,6 +486,11 @@
Arrastrar p\'abaxo p\'anovar
+
+ Desplazase p\'anubrir la barra de ferramientes
+
+ Eslizar pa un llau pa cambiar de llingüeta
+
Sesiones
@@ -502,6 +510,8 @@
Otros marcadoresHistorial
+
+ Llingüeta nuevaLlingüetes sincronizaes
@@ -565,6 +575,8 @@
Amestar una llingüeta
+
+ Amestar una llingüeta privadaEn privao
@@ -592,7 +604,7 @@
Menú de llingüetes abiertes
- Desaniciar la llingüeta de la coleición
+ Quitar la llingüeta de la coleiciónEsbillar llingüetes
@@ -631,7 +643,7 @@
Renomar
- Desaniciar
+ QuitarDesaniciar del historial
@@ -687,14 +699,16 @@
- Desaniciáronse les descargues
+ Quitáronse les descargues
- Desanicióse %1$s
+ Quitóse %1$s
+
+ Nun hai ficheros baxaosDescargues esbillaes: %1$d
- Desaniciar
+ Quitar
@@ -711,6 +725,9 @@
Restaurar la llingüeta
+
+
+ Menú de los marcadores¿De xuru que quies desaniciar esta carpeta?
@@ -821,6 +838,8 @@
Non
+
+ Permitir l\'audiu y videuBloquiar namás l\'audiu y videu colos datos móviles
@@ -950,7 +969,7 @@
Ver
- ¡Amestóse a los sitios destacaos!
+ ¡Amestóse a los sitios principales!Zarróse la llingüeta privada
@@ -960,7 +979,7 @@
DESFACER
- Desanicióse\'l sitiu
+ Quitóse\'l sitiuDesfacer
@@ -1029,6 +1048,8 @@
Llibera espaciu d\'almacenamientuPermisos de sitios
+
+ DescarguesDesaniciar los datos de restolar
@@ -1119,7 +1140,7 @@
La sincronización ta activada
- Fallu al aniciar sesión
+ Hebo un fallu al aniciar sesiónPrivacidá automática
@@ -1153,11 +1174,11 @@
Abrir los axustesLa to privacidá
+
- Diseñemos %s pa date\'l control sobre lo que compartes
- en llinia y con nós.
-
+ Diseñemos %s pa date\'l control tocante a los que compartes
+en llinia y con nós.Lleer el nuesu avisu de privacidá
@@ -1186,14 +1207,14 @@
¡Unvióse la llingüeta!
- Nun pue unviase
+ Nun ye posible unviarRETENTAREscanéu d\'un códigu
- https://firefox.com/pair]]>
+ https://firefox.com/pair]]>Escaniar
@@ -1266,7 +1287,7 @@
Criptomineros
- Buelgues
+ Xeneradores de buelguesBlóquiasePermítese
@@ -1277,13 +1298,13 @@
Cookies de rastrexu ente sitios
- Bloquia les cookies que les redes d\'anuncios y compañes d\'analís usen p\'atropar los tos datos de restolar en milenta sitios.
+ Bloquia les cookies que les redes d\'anuncios y compañes d\'análisis usen p\'atropar los tos datos de restolar en milenta sitios.CriptominerosEvita que los scripts maliciosos consigan accesu al preséu pa minar divises dixitales.
- Buelgues
+ Xeneradores de buelguesEvita la recoyida de datos identificables de forma esclusiva del preséu que puen usase pa rastrexar.
@@ -1401,7 +1422,7 @@
Los anicios de sesión y contraseñes d\'estos sitios nun van guardase.
- Desaniciar toles esceiciones
+ Quitar toles esceicionesBuscar anicios de sesión
@@ -1476,6 +1497,9 @@
Últimu usu
+
+ Menú pa ordenar los anicios sesión
+
Amestar un motor de busca
@@ -1556,11 +1580,11 @@
Nun hai nenguna
- Artículos destacaos
+ Artículos principales¿De xuru que quies desaniciar esti marcador?
- Amestar a los sitios destacaos
+ Amestar a los sitios principalesCola verificación de: %1$s
@@ -1611,9 +1635,9 @@
- Algamóse la llende de sitios destacaos
+ Algamóse la llende de sitios principales
- P\'amestar un sitiu destacáu nuevu, desanicia otru. Ten primíu\'l sitiu y esbilla desaniciar.
+ P\'amestar un sitiu principal nuevu, quita otru. Ten primíu\'l sitiu y esbilla quitar.Val, entendílo
@@ -1623,14 +1647,14 @@
Nome
- Nome del sitiu destacáu
+ Nome del sitiu principalAceutarEncaboxar
- Desaniciar
+ Quitar
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 396daef34..d55cb5f6a 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -1101,6 +1101,8 @@
Вызваліць месцаДазволы для сайтаў
+
+ СцягванніВыдаліць звесткі аглядання
@@ -1223,8 +1225,7 @@
Ваша прыватнасць
- Мы распрацавалі %s, каб даць вам кантроль над тым, чым дзяліцца
- ў Інтэрнэце і тым, чым вы падзеліцеся з намі.
+ Мы распрацавалі %s, каб даць вам кантроль над тым, чым дзяліцца ў Інтэрнэце, і тым, чым вы падзеліцеся з намі.Паведамленне аб прыватнасці
diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml
index 04a0934d2..c40f2a58a 100644
--- a/app/src/main/res/values-bg/strings.xml
+++ b/app/src/main/res/values-bg/strings.xml
@@ -72,14 +72,6 @@
Не, благодаря
-
-
- Стигнете по-бързо до Firefox. Добавете джаджа към началния си екран.
-
- Добавяне на джаджа
-
- Не сега
-
Може да настроите Firefox автоматично да отваря препратки в приложения
@@ -88,6 +80,8 @@
Прекратяване
+
+ Необходим е достъп до камерата. Отворете настройките на Android, докоснете разрешения и изберете разрешаване.Към настройки
@@ -100,6 +94,13 @@
Прекратяване
+
+ Променете изгледа на отворените раздели. Отворете настройки и изберете „мрежа“ в „преглед на раздел“.
+
+ Към настройки
+
+ Отхвърляне
+
Нов раздел
@@ -143,12 +144,12 @@
ИнсталиранеСинхронизирани раздели
+
+ Повторно синхронизиранеТърсене в страницатаПоверителен раздел
-
- Нов разделДобавяне към списък
@@ -213,6 +214,8 @@
Научете повече
+
+ Отваряне на раздел във FirefoxТърсене
@@ -262,7 +265,7 @@
Поверителност и сигурност
- Права на страницата
+ Права на странициПоверително разглеждане
@@ -279,6 +282,8 @@
Личен сървър за Firefox AccountЛичен сървър за Sync
+
+ Сървърът на Firefox Account/Sync е променен. Излизане от приложението за прилагане на промените…Сметка
@@ -341,6 +346,20 @@
Известия
+
+
+ Персонализиран списък от добавки
+
+ Добре
+
+ Отказ
+
+ Наименование на списък
+
+ Собственик на списък (User ID)
+
+ Списъкът с добавки е променен. Излизане от приложението за прилагане на промените…
+
Синхронизиране
@@ -418,6 +437,10 @@
Маркетингови данниСподеля данни за използваните от вас възможности на %1$s чрез Leanplum, нашият партньор за мобилен маркетинг.
+
+ Проучвания
+
+ Позволява на Mozilla да инсталира и изпълнява проучванияЕксперименти
@@ -466,11 +489,19 @@
Зададена от приложение за пестене на батерия
- Следва темата на устройството
+ Като темата на устройството
- Издърпайте за презареждане
+ Издърпване за презареждане
+
+
+ Плъзване скрива лентата с инструменти
+
+ Плъзване на лентата с инструменти настрани сменя раздели
+
+
+ Плъзване нагоре на лентата с инструменти отваря раздели
@@ -519,6 +550,14 @@
Няма затворени раздели
+
+ Раздели
+
+ Изглед на раздели
+
+ Списък
+
+ РешеткаЗатваряне на раздели
@@ -530,6 +569,15 @@
След един месец
+
+ Ръчно затваряне
+
+ Затваряне след един ден
+
+ Затваряне след една седмица
+
+ Затваряне след един месец
+
Отворени раздели
@@ -547,6 +595,8 @@
Отворени разделиДобавяне към списък
+
+ ИзбиранеСподеляне на всички раздели
@@ -561,6 +611,12 @@
Начален екранПревключване режима на раздела
+
+ Отметка
+
+ Затваряне
+
+ Споделяне на избраните разделиПремахване на раздела от спъсъка
@@ -596,8 +652,10 @@
Преименуване на списъкОтваряне на раздели
-
- Премахване
+
+ Преименуване
+
+ ПремахванеПремахване от историята
@@ -633,6 +691,10 @@
is a digit showing the number of items you have selected -->
Изтриване на %1$d записа
+
+ Днес
+
+ ВчераПоследните 24 часа
@@ -646,12 +708,19 @@
Липсва история
-
- Няма изтегляния
+
+ Изтриване на изтеглени файлове
+
+ Сигурни ли сте, че желаете да изтриете изтеглените файлове?%1$d избрани
+
+
+ Отваряне
+
+
Извинете. %1$s не можа да зареди страницата.
@@ -774,6 +843,8 @@
МестоположениеИзвестие
+
+ Постоянно хранилищеВинаги да пита
@@ -905,6 +976,11 @@
Адресът е копиран
+
+ Направи текста в уебсайтовете по-голям или по-малък
+
+ Размер на шрифт
+
Отворени раздели
@@ -919,6 +995,14 @@
%d странициБисквитки
+
+ Буферирани изображения и файлове
+
+ Освобождава място за съхранение
+
+ Права на страницата
+
+ Изтриване на история на разглеждане и изходИзход
@@ -987,10 +1071,6 @@
Отваряне на настройкиПоверителност
-
- Проектирали сме %s така, че да ви предоставя контрол върху това, което споделяте както онлайн, така и с нас.
- Бележка за поверителността
@@ -1254,9 +1334,33 @@
Добре дошли в един чисто нов %s
+
+ Обновяване на %s…
+
+ Стартиране на %s
+
+ Миграцията е завършена
+
+ Пароли
+
+
+ Редактиране
+
+ Паролата е задължителна
+
+ Гласово търсене
+
+ Говорете сега
+
+ Регистрация с това потребителско име вече съществува
+
Добавете друго на устройство.
+
+ Моля, удостоверете се отново.
+
+ Моля, включете синхронизиране на раздели.Няма отворени раздели във Firefox на други ваши устройства.
@@ -1270,6 +1374,13 @@
Добре, разбрах
+
+ Наименование
+
+ Добре
+
+ Отказ
+
Премахване
@@ -1277,4 +1388,11 @@
The first parameter is the name of the app (e.g. Firefox Preview) -->
Извлечете максимума от %s
+
+ Натиснете за подробности
+
+
+ Съберете нещата, които са важни за вас
+
+ Групирайте заедно подобни търсения, сайтове и раздели за бърз достъп по-късно.
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index 0963aabf2..3a39a5928 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -33,6 +33,13 @@
মাল্টিলেক্ট মোড থেকে প্রস্থান করুন
+
+ %1$s নির্বাচিত
+
+ %1$s অনির্বাচিত
+
+ নির্বাচিত
+
%1$s হচ্ছে Mozilla দ্বারা তৈরি।
@@ -925,6 +932,8 @@
আপনি বেশিরভাগ সাইট থেকে লগ আউট হয়ে যাবেনক্যাশেড চিত্র এবং ফাইল
+
+ সংরক্ষণের জায়গা মুক্ত করুনসাইটের অনুমতি
@@ -951,6 +960,9 @@
ব্রাউজিং ডেটা মুছে ফেলা হচ্ছে ...
+
+ নতুন Nightly তে পরিবর্তন করুন
+
@@ -1055,6 +1067,8 @@
কুকিস
+ অদেখা ওয়েবসাইটের কুকিস
+
সমস্ত কুকিস (ওয়েবসাইট ভাঙার কারণ হতে পারে)ট্র্যাকিং কন্টেন্ট
@@ -1100,6 +1114,9 @@
আপনার অধিকার সম্পর্কে জানুন
+
+ লাইসেন্স এর তথ্য
+
1 ট্যাব
@@ -1122,6 +1139,8 @@
বাতিল করুনযোগ করুন
+
+ ওয়েবসাইটে চালিয়ে যানশর্টকাটের নাম
@@ -1144,6 +1163,8 @@
Sync করতে সাইন ইন করুন
+ সংরক্ষিত লগইনগুলো
+
Sync সম্পর্কে আরো জানুন।ব্যতিক্রমসমূহ
diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml
index 542c14329..1a141ea80 100644
--- a/app/src/main/res/values-br/strings.xml
+++ b/app/src/main/res/values-br/strings.xml
@@ -50,14 +50,15 @@
Diuzet
- Gant @fork-maintainers eo produet %1$s.
+ Gant Mozilla eo produet %1$s.En un estez prevez emaocʼh
- %1$s a skarzh ho roll istor klask ha merdeiñ eus an ivinelloù prevez pa guitait anezho pe pa guitait an arload. Daoust ma ne lak ket acʼhanocʼh da vezañ dizanv evit al lecʼhiennoù pe evit ho pourchaser kenrouedad e vo aesocʼh da zercʼhel prevez ar pezh a rit enlinenn evit an dud all a implij an trevnad-mañ.
+
+ %1$s a skarzh ho roll istor klask ha merdeiñ eus an ivinelloù prevez pa guitait anezho pe pa guitait an arload. Daoust ma ne lak ket acʼhanocʼh da vezañ dizanv evit al lecʼhiennoù pe evit ho pourchaser kenrouedad e vo aesocʼh da zercʼhel prevez ar pezh a rit enlinenn evit an dud all a implij an trevnad-mañ.
Mojennoù a vez alies diwar-benn ar merdeiñ prevez
@@ -1081,6 +1082,8 @@
Aotreoù al lec’hienn
+
+ PellgargadurioùDilemel ar roadennoù merdeiñ
@@ -1109,7 +1112,8 @@
Firefox Preview a teu da vezañ Firefox Nighlty
- Bep noz ez eo hizivaet Firefox Nightly evit kinnig keweriusterioù arnodel.
+
+ Bep noz ez eo hizivaet Firefox Nightly evit kinnig keweriusterioù arnodel.
N’eo ket ken stabil avat. Pellgargit ar merdeer beta evit kaout un arload stabiloc’h.Tapit Firefox evit Android Beta
@@ -1117,7 +1121,8 @@
Fiñvet eo Firefox Nightly
- Ne vo ket degemeret hizivadennoù surentez gant an arload-mañ. Paouezit d’e implij ha tremenit war an Nightly nevez.
+
+ Ne vo ket degemeret hizivadennoù surentez gant an arload-mañ. Paouezit d’e implij ha tremenit war an Nightly nevez.
\n\nEvit treuzkas ho sinedoù, titouroù kennaskañ ha roll istor etrezek un arload all, krouit ur gont Firefox.Tremenit d’an Nightly nevez
@@ -1125,7 +1130,8 @@
Fiñvet eo Firefox Nightly
- Ne vo ket degemeret hizivadennoù surentez gant an arload-mañ. Paouezit d’e implij ha tremenit war an Nightly nevez.
+
+ Ne vo ket degemeret hizivadennoù surentez gant an arload-mañ. Paouezit d’e implij ha tremenit war an Nightly nevez.
\n\nEvit treuzkas ho sinedoù, titouroù kennaskañ ha roll istor etrezek un arload all, krouit ur gont Firefox.
@@ -1201,8 +1207,7 @@
Ho puhez prevez
- Savet hon eus %s evit ma c’hallfec’h reoliñ ar pezh a rannit
- enlinenn hag ar pezh a rannit ganeomp.
+ Savet hon eus %s evit ma c’hallfec’h reoliñ ar pezh a rannit enlinenn hag ar pezh a rannit ganeomp.Lennit hon evezhiadenn a-get buhez prevez
@@ -1227,9 +1232,9 @@
Neuz sklaer
- Ivinelloù kaset
+ Ivinelloù kaset!
- Ivinell kaset
+ Ivinell kaset!Ne c’haller ket kas
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index 2b191cdd9..b4b7cb5ff 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -129,6 +129,8 @@
Edita l’adreça d’interèsComplements
+
+ ExtensionsNo hi ha cap complement
@@ -530,6 +532,10 @@
Altres adreces d’interèsHistorial
+
+ Pestanya nova
+
+ Cerca a la pàginaPestanyes sincronitzades
@@ -726,10 +732,12 @@
Suprimeix les baixadesSegur que voleu esborrar les baixades?
-
- S’han suprimit les baixades
+
+ S’han eliminat les baixades
+
+ S’ha eliminat %1$s
- No hi ha cap baixada
+ No hi ha cap fitxer baixat%1$d seleccionades
@@ -737,8 +745,10 @@
Obre
-
- Suprimeix
+
+
+
+ Elimina
@@ -862,6 +872,8 @@
NotificacióEmmagatzematge persistent
+
+ Contingut controlat per DRMDemana-m’ho
@@ -1086,6 +1098,8 @@
Permisos dels llocs
+
+ BaixadesSuprimeix les dades de navegació
@@ -1208,8 +1222,7 @@
La vostra privadesa
- Hem dissenyat el %s per donar-vos el control sobre tot allò que compartiu en línia i que compartiu amb nosaltres.
-
+ Hem dissenyat el %s per donar-vos el control sobre tot allò que compartiu en línia i que compartiu amb nosaltres.Mostra l’avís de privadesa
diff --git a/app/src/main/res/values-co/strings.xml b/app/src/main/res/values-co/strings.xml
index 2559443ce..c42c30624 100644
--- a/app/src/main/res/values-co/strings.xml
+++ b/app/src/main/res/values-co/strings.xml
@@ -135,6 +135,8 @@
Mudificà l’indettaModuli addiziunali
+
+ EstensioniNisunu modulu quì
@@ -428,7 +430,7 @@
Prutezzione contr’à u spiunagiu
- Blocca u cuntenutu è i scritti chì vò seguitanu in linea.
+ Blocca u cuntenutu è i scenarii chì vò seguitanu in linea.Eccezzioni
@@ -534,6 +536,10 @@
Altre indetteCronolugia
+
+ Nova unghjetta
+
+ Circà in a paginaUnghjette sincrunizate
@@ -1097,6 +1103,8 @@
Libereghja spaziu d’allucamentuPermessi di situ
+
+ ScaricamentiSquassà i dati di navigazione
@@ -1222,9 +1230,7 @@
A vostra vita privata
- Avemu cuncivitu %s per davvi u cuntrollu nant’à ciò chì vò spartite¶
- in linea è nant’à ciò chì vò spartite cù noi.¶
-
+ Avemu cuncivitu %s per davvi u cuntrollu nant’à ciò chì vò spartite in linea è nant’à ciò chì vò spartite cù noi.Leghjite a nostra pulitica di cunfidenzialità
@@ -1306,7 +1312,7 @@
Persunalizata
- Sciglite i perseguitatori è i scritti à bluccà.
+ Sciglite i perseguitatori è i sscenarii à bluccà.Ciò chì hè bluccatu da a prutezzione persunalizata contr’à u spiunagiu
@@ -1346,7 +1352,7 @@
Minatori di crittomuneta
- Impedisce i scritti gattivi d’accede à u vostru apparechju per « estrae » muneta numerica.
+ Impedisce i scenarii gattivi d’accede à u vostru apparechju per « estrae » muneta numerica.Detettori d’impronta numerica
@@ -1354,7 +1360,7 @@
Cuntenutu impiegatu per u spiunagiu
- Impedisce u caricamentu di e publicità, video è altri cuntenuti d’urigine esterna à u situ è cuntenendu codice di spiunagiu. Quessu pò affettà certe funzioni di u situ web.
+ Impedisce u caricamentu di e publicità, video è altri cuntenuti d’origine esterna à u situ è cuntenendu codice di spiunagiu. Què pò affettà certe funzioni di u situ web.Quandu u scudu hè di culore viulettu, vole si dì chì %s hà bluccatu perseguitatori nant’à u situ visitatu. Picchichjate per sapene di più.
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 736a0b90e..5d3a4477e 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -131,6 +131,8 @@
Upravit záložkuDoplňky
+
+ RozšířeníŽádné doplňky
@@ -537,6 +539,10 @@
Ostatní záložkyHistorie
+
+ Nový panel
+
+ Najít na stránceSynchronizované panely
@@ -1229,8 +1235,7 @@
Vaše soukromí
- %s vám dává kontrolu nad tím, co sdílíte online a co sdílíte s námi.
-
+ %s vám dává kontrolu nad tím, co sdílíte online a co sdílíte s námi.Přečíst zásady ochrany osobních údajů
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index 4f07b759f..1ee1be2a5 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -130,6 +130,8 @@
Golygu nod tudalenYchwanegion
+
+ EstyniadauDim ychwanegion yma
@@ -527,6 +529,10 @@
Nodau Tudalen EraillHanes
+
+ Tab newydd
+
+ Canfod yn y dudalenTabiau wedi’u Cydweddu
@@ -1087,6 +1093,8 @@
Yn rhyddhau lle storioCaniatâd gwefan
+
+ LlwythiDileu data pori
@@ -1209,8 +1217,7 @@ Fodd bynnag, gall fod yn llai sefydlog. Llwythwch ein porwr Beta i gael profiad
Eich preifatrwydd
- Rydym wedi cynllunio %s i roi rheolaeth i chi dros yr hyn rydych chi’n ei rannu
- ar-lein a’r hyn rydych chi’n ei rannu gyda ni.
+ Rydym wedi cynllunio %s i roi rheolaeth i chi dros yr hyn rydych chi’n ei rannu ar-lein a’r hyn rydych chi’n ei rannu gyda ni.
Darllenwch ein hysbysiad preifatrwydd
diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml
index 0e01ba854..838fe1f53 100644
--- a/app/src/main/res/values-da/strings.xml
+++ b/app/src/main/res/values-da/strings.xml
@@ -1079,6 +1079,8 @@
Frigør lagerpladsWebsteds-indstillinger
+
+ FilhentningerSlet browserdata
@@ -1201,8 +1203,7 @@
Bedre beskyttelse af dit privatliv
- Vi har designet %s til at give dig kontrol over, hvad du deler på
- nettet - og hvad du deler med os.
+ Vi har designet %s til at give dig kontrol over, hvad du deler på nettet - og hvad du deler med os.Læs vores privatlivspolitik
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index ac58d84e2..0f8097498 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -55,7 +55,7 @@
Ausgewählt
- %1$s wird von @fork-maintainers hergestellt.
+ %1$s wird von Mozilla hergestellt.
@@ -133,6 +133,8 @@
Lesezeichen bearbeitenAdd-ons
+
+ ErweiterungenHier sind keine Add-ons
@@ -537,6 +539,10 @@
Weitere LesezeichenChronik
+
+ Neuer Tab
+
+ In Seite suchenSynchronisierte Tabs
@@ -1121,6 +1127,8 @@
Gibt Speicherplatz freiWebsite-Berechtigungen
+
+ DownloadsBrowser-Daten löschen
@@ -1243,7 +1251,7 @@
Ihre Privatsphäre
- Wir haben %s so konzipiert, dass Sie die Kontrolle darüber haben, was Sie im Internet und was Sie mit uns teilen.
+ Wir haben %s so konzipiert, dass Sie die Kontrolle darüber haben, was Sie im Internet und was Sie mit uns teilen.Lesen Sie unseren Datenschutzhinweis
diff --git a/app/src/main/res/values-dsb/strings.xml b/app/src/main/res/values-dsb/strings.xml
index aa3cec121..021686f6b 100644
--- a/app/src/main/res/values-dsb/strings.xml
+++ b/app/src/main/res/values-dsb/strings.xml
@@ -130,6 +130,8 @@
Cytańske znamje wobźěłaśDodanki
+
+ RozšyrjenjaŽedne dodanki how
@@ -527,6 +529,10 @@
Druge cytańske znamjenjaHistorija
+
+ Nowy rejtarik
+
+ Na boku pytaśSynchronizěrowane rejtariki
@@ -1086,6 +1092,8 @@
Pušćijo składowański rumSedłowe pšawa
+
+ ZeśěgnjenjaPśeglědowańske daty wulašowaś
@@ -1209,9 +1217,7 @@
Waša priwatnosć
- Smy %s wuwili, aby wam kontrolu wó tom dali, co online
- źěliśo a co z nami.
-
+ Smy %s wuwili, aby wam kontrolu wó tom dali, co online źěliśo a co z nami.Cytajśo našu powěźeńku priwatnosći
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 7e208ffc2..345827d89 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -135,6 +135,8 @@
Επεξεργασία σελιδοδείκτηΠρόσθετα
+
+ ΕπεκτάσειςΔεν βρέθηκαν πρόσθετα
@@ -536,6 +538,10 @@
Άλλοι σελιδοδείκτεςΙστορικό
+
+ Νέα καρτέλα
+
+ Εύρεση στη σελίδαΣυγχρονισμένες καρτέλες
@@ -1100,6 +1106,8 @@
Απελευθερώνει χώρο αποθήκευσηςΔικαιώματα ιστοσελίδων
+
+ ΛήψειςΔιαγραφή δεδομένων περιήγησης
@@ -1226,9 +1234,7 @@
Το απόρρητό σας
- Έχουμε σχεδιάσει το %s ώστε να έχετε τον έλεγχο του τι θα κοινοποιείτε
- στο διαδίκτυο και σε εμάς.
-
+ Έχουμε σχεδιάσει το %s έτσι, ώστε να ελέγχετε τι κοινοποιείτε στο διαδίκτυο και σε εμάς.Διαβάστε τη σημείωση απορρήτου μας
diff --git a/app/src/main/res/values-en-rCA/strings.xml b/app/src/main/res/values-en-rCA/strings.xml
index 13d74fef2..03c68fca0 100644
--- a/app/src/main/res/values-en-rCA/strings.xml
+++ b/app/src/main/res/values-en-rCA/strings.xml
@@ -20,6 +20,10 @@
Your private tabs will be shown here.
+
+ Baidu
+
+ JD1 open tab. Tap to switch tabs.
@@ -71,14 +75,6 @@
No thanks
-
-
- Get to Firefox faster. Add a widget to your Home screen.
-
- Add widget
-
- Not now
-
You can set Firefox to automatically open links in apps.
@@ -101,6 +97,13 @@
Dismiss
+
+ Change the layout of open tabs. Go to settings and select grid under tab view.
+
+ Go to settings
+
+ Dismiss
+
New tab
@@ -151,8 +154,6 @@
Find in pagePrivate tab
-
- New tabSave to collection
@@ -359,6 +360,12 @@
Add-on collection modified. Quitting the application to apply changes…
+
+
+ Add-on is not supported
+
+ Add-on is already installed
+
Sync now
@@ -437,6 +444,10 @@
Marketing dataShares data about what features you use in %1$s with Leanplum, our mobile marketing vendor.
+
+ Studies
+
+ Allows Mozilla to install and run studiesExperiments
@@ -559,6 +570,15 @@
After one month
+
+ Close manually
+
+ Close after one day
+
+ Close after one week
+
+ Close after one month
+
Open tabs
@@ -576,6 +596,8 @@
Open TabsSave to collection
+
+ SelectShare all tabs
@@ -590,8 +612,18 @@
Go homeToggle tab mode
+
+ Bookmark
+
+ Close
+
+ Share selected tabs
+
+ Selected tabs menuRemove tab from collection
+
+ Select tabsClose tab
@@ -627,8 +659,10 @@
Collection name
-
- Remove
+
+ Rename
+
+ RemoveDelete from history
@@ -681,12 +715,27 @@
No history here
+
+ Delete downloads
+
+ Are you sure you want to clear your downloads?
+
+ Downloads Removed
+
+ Removed %1$s
- No downloads here
+ No downloaded files%1$d selected
+
+
+ Open
+
+ Remove
+
+
Sorry. %1$s can’t load that page.
@@ -808,6 +857,8 @@
NotificationPersistent Storage
+
+ DRM-controlled contentAsk to allow
@@ -947,6 +998,12 @@
Tab closedTabs closed
+
+ Tabs closed!
+
+ Bookmarks saved!
+
+ ViewAdded to top sites!
@@ -1022,6 +1079,8 @@
Frees up storage spaceSite permissions
+
+ DownloadsDelete browsing data
@@ -1143,9 +1202,7 @@
Your privacy
- We’ve designed %s to give you control over what you share
- online and what you share with us.
-
+ We’ve designed %s to give you control over what you share online and what you share with us.Read our privacy notice
@@ -1603,6 +1660,15 @@
Show most visited sites
+
+ Name
+
+ Top site name
+
+ OK
+
+ Cancel
+
Remove
diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml
index 7e5d50b62..49d293b86 100644
--- a/app/src/main/res/values-en-rGB/strings.xml
+++ b/app/src/main/res/values-en-rGB/strings.xml
@@ -129,6 +129,8 @@
Edit bookmarkAdd-ons
+
+ ExtensionsNo add-ons here
@@ -524,6 +526,10 @@
Other BookmarksHistory
+
+ New tab
+
+ Find in pageSynchronised tabs
@@ -1082,6 +1088,8 @@
Frees up storage spaceSite permissions
+
+ DownloadsDelete browsing data
@@ -1202,9 +1210,7 @@
Your privacy
- We’ve designed %s to give you control over what you share
- online and what you share with us.
-
+ We’ve designed %s to give you control over what you share online and what you share with us.Read our privacy notice
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index a32a44b64..fc99a3785 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -127,6 +127,8 @@
Redakti legosignonAldonaĵoj
+
+ EtendaĵojNeniu aldonaĵo estas ĉi tie
@@ -527,6 +529,10 @@
Aliaj legosignojHistorio
+
+ Nova langeto
+
+ Serĉi en la paĝoSpegulitaj langetoj
@@ -1096,6 +1102,8 @@
Permesoj por retejoj
+
+ ElŝutojForigi retumajn datumojn
@@ -1218,8 +1226,7 @@
Via privateco
- Ni kreis %s por doni al vi la eblon plene regi kion vi dividas
-en la reto kaj kion vi dividas kun ni.
+ Ni kreis %s por doni al vi la eblon plene regi kion vi dividas en la reto kaj kion vi dividas kun ni.Legu nian rimarkon pri privateco
diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml
index 1970a118c..d2126b692 100644
--- a/app/src/main/res/values-es-rAR/strings.xml
+++ b/app/src/main/res/values-es-rAR/strings.xml
@@ -131,6 +131,8 @@
Editar marcadorComplementos
+
+ ExtensionesNo hay complementos aquí
@@ -540,6 +542,10 @@
Otros marcadoresHistorial
+
+ Nueva pestaña
+
+ Buscar en la páginaPestañas sincronizadas
@@ -1109,6 +1115,8 @@
Libera espacio de almacenamientoPermisos del sitio
+
+ DescargasEliminar datos de navegación
@@ -1229,7 +1237,7 @@
- Diseñamos %s para que puedas controlar lo que compartís en línea y lo que compartís con nosotros.
+ Diseñamos %s para que puedas controlar lo que compartís en línea y lo que compartís con nosotros.Leé nuestra política de privacidad
diff --git a/app/src/main/res/values-es-rCL/strings.xml b/app/src/main/res/values-es-rCL/strings.xml
index 2b86e0852..de1528334 100644
--- a/app/src/main/res/values-es-rCL/strings.xml
+++ b/app/src/main/res/values-es-rCL/strings.xml
@@ -1084,6 +1084,8 @@
Libera espacio de almacenamientoPermisos de sitios
+
+ DescargasEliminar datos de navegación
@@ -1206,9 +1208,7 @@
- Hemos diseñado %s para darte el control sobre lo que compartes
- línea y lo que compartes con nosotros.
-
+ Hemos diseñado %s para darte el control sobre lo que compartes en línea y lo que compartes con nosotros.Lee nuestro aviso de privacidad
diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml
index 3175a57d1..0f8730760 100644
--- a/app/src/main/res/values-es-rES/strings.xml
+++ b/app/src/main/res/values-es-rES/strings.xml
@@ -132,6 +132,8 @@
Editar marcadorComplementos
+
+ ExtensionesNo hay complementos aquí
@@ -538,6 +540,10 @@
Otros marcadoresHistorial
+
+ Nueva pestaña
+
+ Buscar en la páginaPestañas sincronizadas
@@ -1119,6 +1125,8 @@
Libera espacio de almacenamientoPermisos del sitio
+
+ DescargasEliminar datos de navegación
@@ -1243,8 +1251,7 @@
Tu privacidad
- Hemos diseñado %s para darte control sobre lo que compartes
- en línea y lo que compartes con nosotros.
+ Hemos diseñado %s para darte el control sobre lo que compartes en línea y lo que compartes con nosotros.Lee nuestro aviso de privacidad
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 4b2d2586f..ad65854cf 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -57,7 +57,7 @@
Seleccionado
- %1$s es producido por @fork-maintainers.
+ %1$s es producido por Mozilla.
@@ -1119,6 +1119,8 @@
Libera espacio de almacenamientoPermisos de los sitios
+
+ DescargasEliminar datos de navegación
@@ -1243,9 +1245,7 @@
Tu privacidad
- Hemos diseñado %s para darte control sobre lo que compartes
- en línea y lo que compartes con nosotros.
-
+ Hemos diseñado %s para darte el control sobre lo que compartes en línea y lo que compartes con nosotros.Lee nuestro aviso de privacidad
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index 6416e94a6..2e8280356 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -1101,6 +1101,8 @@
Biltegiratze tokia libratzen duGunearen baimenak
+
+ DeskargakEzabatu nabigatze-datuak
@@ -1224,9 +1226,7 @@
Zure pribatutasuna
- Online partekatzen duzunaren eta gurekin partekatzen
- duzunaren inguruko kontrola emateko diseinatu dugu %s.
-
+ Online partekatzen duzunaren eta gurekin partekatzen duzunaren inguruko kontrola emateko diseinatu dugu %s.Irakurri gure pribatutasun-oharra
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index ca33925c9..b0fb3425e 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -8,9 +8,9 @@
گزینههای بیشتر
- فعال شده در حالت مرور خصوصی
+ فعال کردن حالت خصوصی
- غیر فعال شده در حالت مرور خصوصی
+ غیرفعال کردن حالت مرور خصوصیجستوجو یا ورود آدرس
@@ -24,7 +24,7 @@
JD
- 1 زبانه باز. برای تغییر زبانه ها ضربه بزنید.
+ 1 زبانهي باز. برای تغییر زبانه ها ضربه بزنید.%1$s زبانههای باز. برای تغییر زبانهها ضربه بزنید.
@@ -68,9 +68,9 @@
برای باز کردن زبانه های خصوصی از صفحه اصلی ، میانبر اضافه کنید.
- میانبر را اضافه کنید
+ افزودن میانبر
- نه، ممنون!
+ نه، ممنون
@@ -192,13 +192,13 @@
- اسکن
+ بررسیموتور جستوجوتنظیمات موتور جستجو
- این بار، جستوجو با:
+ این بار بگرد با:پیوند را از کلیپ بورد پر کنید
@@ -493,6 +493,11 @@
موضوع دستگاه را دنبال کنید
+
+
+ برای تازه سازی صفحه بکشید
+
+ برای پنهان کردن نوار ابزار پیمایش کنیدبرای جابجایی بین زبانهها، نوار ابزار را به طرفین بکشید
@@ -1078,6 +1083,8 @@
فضای ذخیره سازی را آزاد می کندمجوزهای سایت
+
+ دریافتهاداده مرور را حذف کنید
@@ -1148,6 +1155,10 @@
شروع به همگام سازی نشانکها، گذرواژه ها و چیزهای بیشتر با حساب Firefox شما.اطلاعات بیشتر
+
+ شما به عنوان%s در یک مرورگر دیگر Firefox در این تلفن وارد شده اید. آیا می خواهید با این حساب وارد شوید؟بله، من را وارد کن
@@ -1195,8 +1206,7 @@
حریم خصوصی شما
- ما%s طراحی کرده ایم تا بتوانیم بر آنچه به اشتراک می گذارید کنترل کنید
- آنلاین و آنچه را با ما به اشتراک می گذارید.
+ ما%s طراحی کرده ایم تا بتوانیم بر آنچه به صورت انلاین یا با ما اشتراک می گذارید کنترل داشته باشید.اعلامیه حریم خصوصی ما را بخوانید
@@ -1397,6 +1407,9 @@
نام میانبر
+
+ شما به راحتی میتوانید این پایگاه اینترنتی را به صفحه خانگی تلفن خود اضافه کنید تا دسترسی مستقیم به آن داشته باشید و مرور سریعتی را مانند تجربه کردن یک برنامه داشته باشید.
+
ورودها و گذرواژهها
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 21c4f170b..d672e5818 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -131,6 +131,8 @@
Muokkaa kirjanmerkkiäLisäosat
+
+ LaajennuksetEi lisäosia täällä
@@ -535,6 +537,10 @@
Muut kirjanmerkitHistoria
+
+ Uusi välilehti
+
+ Etsi sivultaSynkronoidut välilehdet
@@ -1100,6 +1106,8 @@
Vapauttaa tallennustilaaSivustojen käyttöoikeudet
+
+ LatauksetPoista selaustiedot
@@ -1224,9 +1232,7 @@
Yksityisyytesi
- %s on suunniteltu antamaan sinulle päätösvalta sen suhteen,
- mitä jaat verkossa ja mitä jaat kanssamme.
-
+ Olemme suunnitelleet %sin siten, että voit hallita mitä jaat verkossa ja mitä jaat kanssamme.Lue yksityisyyskäytäntömme
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 6c27b1cdc..808ade139 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -1119,6 +1119,8 @@
Libère de l’espace de stockagePermissions des sites
+
+ TéléchargementsSupprimer les données de navigation
@@ -1245,9 +1247,7 @@ Cependant, il peut être moins stable. Téléchargez la version bêta de notre n
Votre vie privée
- Nous avons conçu %s pour vous donner le contrôle sur ce que vous partagez
- en ligne et sur ce que vous partagez avec nous.
-
+ Nous avons conçu %s pour vous donner le contrôle de ce que vous partagez en ligne et de ce que vous partagez avec nous.Consulter notre politique de confidentialité
diff --git a/app/src/main/res/values-fy-rNL/strings.xml b/app/src/main/res/values-fy-rNL/strings.xml
index fb28ee69c..4f33c21e0 100644
--- a/app/src/main/res/values-fy-rNL/strings.xml
+++ b/app/src/main/res/values-fy-rNL/strings.xml
@@ -132,6 +132,8 @@
Blêdwizer bewurkjeAdd-ons
+
+ UtwreidingenGjin add-ons hjir
@@ -528,6 +530,10 @@
Oare blêdwizersSkiednis
+
+ Nij ljepblêd
+
+ Sykje op sideSyngronisearre ljepblêden
@@ -1087,6 +1093,8 @@
Meitsje ûnthâldromte frijWebsitemachtigingen
+
+ DownloadsNavigaasjegegevens fuortsmite
@@ -1207,9 +1215,7 @@
Jo privacy
- Wy hawwe %s ûntwurpen om jo kontrôle te jaan oer wat jo online
-
- diele en wat jo mei ús diele.
+ Wy hawwe %s ûntwurpen om jo kontrôle te jaan oer wat jo online diele en wat jo mei ús diele.
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 6959b8fea..cd25c2457 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -161,6 +161,12 @@
Aberto en %1$s
+
+ CREADO POR %1$s
+
+ Creado por %1$sModo de lectura
@@ -971,6 +977,8 @@
AbrirEliminar e abrir
+
+ Creado porColección eliminada
@@ -1066,6 +1074,8 @@
Libera espazo de almacenamentoPermisos do sitio
+
+ DescargasEliminar datos de navegación
@@ -1188,6 +1198,11 @@
A súa privacidade
+
+ Deseñamos %s para darlle o control sobre o que comparte en liña e o que comparte con nós.
+
+ Lea a nosa política de privacidadePechar
@@ -1479,6 +1494,10 @@
Configurar agoraDesbloquear o dispositivo
+
+ Zoom sobre todos os sitios
+
+ Activa o permiso para toque e aumento, incluso en sitios web que bloquean este xesto.Nome (A-Z)
@@ -1513,4 +1532,151 @@
Detalles do motor de busca personalizado
-
+
+ Ligazón a Obter máis información…
+
+
+ Introduza o nome do buscador
+
+ Xa existe un motor de busca co nome «%s».
+
+ Introduza unha cadea de busca
+
+ Comprobe que a cadea de busca coincida co formato do exemplo
+
+ Erro ao conectar con «%s»
+
+ Creouse %s
+
+ Gardouse %s
+
+ Eliminouse %s
+
+
+ Benvida a un %s completamente novo
+
+ Agarda un navegador completamente redeseñado, cun mellor rendemento e funcións para axudar a facer máis en liña.\\n Agarde mentres actualizamos %s con isto:
+
+ Actualizando %s…
+
+ Iniciar %s
+
+ Migración completada
+
+ Contrasinais
+
+
+ Para o permitir:
+
+
+ 1. Vaia a Configuración de Android
+
+ Permisos]]>
+
+ %1$s a ACTIVADO]]>
+
+
+ Conexión segura
+
+ Conexión insegura
+
+ Confirma que desexa limpar todos os permisos de todos os sitios?
+
+ Confirma que desexa limpar todos os permisos deste sitio?
+
+ Confirma que desexa limpar este permiso deste sitio?
+
+ Non hai excepcións no sitio
+
+ Artigos principais
+
+ Confirma que quere eliminar este marcador?
+
+ Engadido aos sitios principais
+
+ Comprobado por: %1$s
+
+ Eliminar
+
+ Editar
+
+ Confirma que desexa eliminar este inicio de sesión?
+
+ Eliminar
+
+ Opcións de inicio de sesión
+
+ O campo de texto editábel para o enderezo web do inicio de sesión.
+
+ O campo de texto editábel para o nome de usuario do inicio de sesión.
+
+ O campo de texto editábel para o contrasinal do inicio de sesión.
+
+ Gardar os cambios para iniciar sesión.
+
+ Descartar cambios
+
+ Editar
+
+ Contrasinal obrigatorio
+
+ Busca por voz
+
+ Fale agora
+
+ Xa existe un inicio de sesión con ese nome
+
+
+
+ Conectar outro dispositivo.
+
+ Autentíquese de novo.
+
+ Active a sincronización de lapelas.
+
+ Non ten ningunha lapela aberta en Firefox nos outros dispositivos.
+
+ Ver unha lista das lapelas dos outros dispositivos.
+
+ Inicie sesión para sincronizar
+
+ Non hai lapelas abertas
+
+
+
+ Acadouse o límite superior do sitio
+
+ Para engadir un novo sitio primario, retire outro. Toque e manteña a presión sobre o sitio e seleccione retiralo.
+
+ De acordo, entendín
+
+ Amosar os sitios máis visitados
+
+ Nome
+
+ Nome do sitio primario
+
+ De acordo
+
+ Cancelar
+
+
+ Retirar
+
+
+ Obter o máximo de %s.
+
+
+ Premer para obter máis detalles
+
+
+ Colla as cousas que lle importan
+
+ Agrupar xuntas as buscar semellantes, sitios e lapelas para ter un acceso rápido despois.
+
+ Iniciou sesión como %s noutro navegador Firefox neste móbil. Confirma que quere iniciar sesión con esta conta?
+
+ Pode engadir doadamente este sitio web á pantalla principal do seu móbil para ter acceso instantáneo e navegar máis rápido cunha experiencia de tipo app.
+
diff --git a/app/src/main/res/values-gn/strings.xml b/app/src/main/res/values-gn/strings.xml
index c0b7e3540..66e7e9225 100644
--- a/app/src/main/res/values-gn/strings.xml
+++ b/app/src/main/res/values-gn/strings.xml
@@ -132,6 +132,8 @@
Techaukaha mbosako’iMoĩmbaha
+
+ JepysokueNdaipóri moĩmbaha ápe
@@ -534,6 +536,10 @@
Ambue techaukahakuéraTembiasakue
+
+ Tendayke pyahu
+
+ Eheka kuatiaroguépeTendayke mbojuehepyre
@@ -1107,6 +1113,8 @@
Emopotĩ pa’ũ ñembyatyhaTenda ñemoneĩ
+
+ ÑemboguejyEmboguete kundahára mba’ekuaarã
@@ -1230,8 +1238,7 @@
- Romoha’ãngáma %s eñangareko hag̃ua umi emoherakuãva rehe
- ñandutípe ha emoherakuã orendive.
+ Rojapo %s eñangareko hag̃ua emoherakuãva ñandutípe rehe ha emoherakuãva orendive avei.Emoñe’ẽ ore marandu’i ñemigua
diff --git a/app/src/main/res/values-hi-rIN/strings.xml b/app/src/main/res/values-hi-rIN/strings.xml
index 5db21d023..58d511663 100644
--- a/app/src/main/res/values-hi-rIN/strings.xml
+++ b/app/src/main/res/values-hi-rIN/strings.xml
@@ -20,6 +20,8 @@
आपके निजी टैब यहाँ दिखाए जाएंगे।
+
+ JD1 खुला टैब। टैब स्विच करने के लिए टैप करें।
@@ -70,20 +72,15 @@
नहीं धन्यवाद
-
-
- ओर तेज Firefox करने के लिए, अपने होम स्क्रीन पर एक विजेट जोड़ें।
-
- विजेट जोड़ें
-
- अभी नहीं
-
सेटिंग पर जाएंसेटिंग पर जाएं
+
+ सेटिंग पर जाएं
+
नया टैब
@@ -132,8 +129,6 @@
पृष्ठ में ढूँढेंनिजी टैब
-
- नया टैबसंग्रहण में सहेजें
@@ -200,6 +195,8 @@
अधिक जानें
+
+ एक नया Firefox टैब खोलेंखोजें
@@ -273,6 +270,8 @@
टूलबारथीम
+
+ मुख्य पृष्ठअनुकूलित करें
@@ -308,6 +307,8 @@
ब्राउज़िंग इतिहास खोजेंबुकमार्क खोजें
+
+ सिंक किए गए टैब खोजेंखाता सेटिंग
@@ -318,6 +319,12 @@
अधिसूचनाएं
+
+ रद्द करें
+
+
+ ऐड-ऑन पहले से इंस्टॉल है
+
अब सिंक करें
@@ -491,6 +498,8 @@
%d टैब
+
+ टैबटैब बंद करें
@@ -531,6 +540,10 @@
मुख्य पृष्ठ पर जाएंटैब मोड को टॉगल करें
+
+ बुकमार्क
+
+ बंद करेंसंग्रहण से टैब हटायें
@@ -567,7 +580,7 @@
खुले टैब
- हटाएं
+ हटाएंइतिहास से मिटाएं
@@ -615,6 +628,7 @@
यहां कोई इतिहास नहीं
+
माफ़ कीजिए। %1$s उस पृष्ठ को लोड नहीं कर सकता हैं।
@@ -878,6 +892,8 @@
टैब बंद हो गयाटैब बंद हो गया
+
+ बुकमार्क सहेजा गया!शीर्ष साइटों में जोड़ा गया!
@@ -1070,10 +1086,6 @@
सेटिंग खोलेंआपकी गोपनीयता
-
- आपने जो हमारे साथ साझा करते है या जो आप ऑनलाइन साझा करते हैं
- उस पर नियंत्रण देने के लिए हमने %s डिज़ाइन किया हैं।हमारी गोपनीयता सूचना पढ़ें
@@ -1535,10 +1547,18 @@
ठीक है, समझ गए
+
+ नाम
+
+ रद्द करें
+
%s का अधिकतम लाभ उठाएं।
+
+ अधिक जानकारी के लिए क्लिक करें
+
जो चीजें आपके लिए मायने रखती हैं, उन्हें इकट्ठा करें
diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml
index 28642f388..a2f96eb21 100644
--- a/app/src/main/res/values-hr/strings.xml
+++ b/app/src/main/res/values-hr/strings.xml
@@ -129,6 +129,8 @@
Uredi zabilješkuDodaci
+
+ ProširenjaOvdje nema dodataka
@@ -290,7 +292,7 @@
Alatna traka
- Motiv
+ TemaPočetna
@@ -499,7 +501,7 @@
Postavljeno u postavkama za štednju baterije
- Slijedi motiv uređaja
+ Slijedi temu uređaja
@@ -530,6 +532,10 @@
Druge zabilješkePovijest
+
+ Nova kartica
+
+ Pronađi na straniciSinkronizirane kartice
@@ -889,13 +895,13 @@
Dopusti zvuk i video
- Blokiraj audio i video samo na mobilnoj vezi
+ Blokiraj zvuk i video samo na mobilnoj vezi
- Audio i video će se reproducirati na Wi-Fi
+ Zvuk i video reproducirat će se na Wi-Fi-ju
- Blokiraj samo audio
+ Blokiraj samo zvuk
- Blokiraj audio i video
+ Blokiraj zvuk i videoUključeno
@@ -1094,6 +1100,8 @@
Prazni memorijuDozvole web-stranice
+
+ PreuzimanjaIzbriši podatke pregledavanja
@@ -1217,8 +1225,7 @@
- Dizajnirali smo %s tako, da ti vladaš onime što dijeliš
- na internetu i što dijeliš s nama.
+ %s smo stvorili za jednostavno upravljanje podacima koje dijeliš na mreži i koje dijeliš s nama.Pročitaj naša pravila privatnosti
@@ -1232,16 +1239,16 @@
Odaberi modus
- Štedi energiju i zaštiti oči pomoću tamnog motiva.
+ Štedi energiju i zaštiti oči pomoću tamne teme.AutomatskiPrilagođava se postavkama uređaja
- Tamni motiv
+ Tamna tema
- Svijetli motiv
+ Svijetla temaKartice su poslane!
diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml
index 47b54eaf5..366e974d2 100644
--- a/app/src/main/res/values-hsb/strings.xml
+++ b/app/src/main/res/values-hsb/strings.xml
@@ -131,6 +131,8 @@
Zapołožku wobdźěłaćPřidatki
+
+ RozšěrjenjaŽane přidatki tu
@@ -529,6 +531,10 @@
Druhe zapołožkiHistorija
+
+ Nowy rajtark
+
+ Na stronje pytaćSynchronizowane rajtarki
@@ -1090,6 +1096,8 @@
Wuswobodźa składowanski rumSydłowe prawa
+
+ SćehnjenjaPřehladowanske daty zhašeć
@@ -1212,9 +1220,7 @@
Waša priwatnosć
- Smy %s wuwili, zo bychmy wam kontrolu wo tym dali, što online
- dźěliće a što z nami.
-
+ Sym %s wuwili, zo bychmy wam kontrolu wo tym dali, što online dźěliće a što z nami.Čitajće naš zdźělenku priwatnosće
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index a2ba405b0..9010518a1 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -1094,6 +1094,8 @@
Felszabadítja a tárhelyetWebhely engedélyek
+
+ LetöltésekBöngészési adatok törlése
@@ -1219,8 +1221,7 @@
Adatvédelem
- Úgy terveztük a %s böngészőt, hogy irányítást adjunk afelett, hogy mit oszt
- meg online, és mit oszt meg velünk.
+ Úgy terveztük a %s böngészőt, hogy irányítást adjunk afelett, hogy mit oszt meg online, és mit oszt meg velünk.
Olvassa el az adatvédelmi nyilatkozatunkat
diff --git a/app/src/main/res/values-hy-rAM/strings.xml b/app/src/main/res/values-hy-rAM/strings.xml
index 5e407c577..a0a291e3b 100644
--- a/app/src/main/res/values-hy-rAM/strings.xml
+++ b/app/src/main/res/values-hy-rAM/strings.xml
@@ -1088,6 +1088,8 @@
Ազատում է պահեստային տարածքԿայքերի թույլտվություններ
+
+ ՆերբեռնումներՋնջել դիտարկման տվյալները
@@ -1210,8 +1212,7 @@
- Մենք պատրաստել ենք %s-ը, որպեսզի Ձեզ տանք հսկողություն սահմանելը այն ամենի նկատմամբ, ինչ որ համօգտագործում եք
- առցանց և մեզ հետ:
+ Մենք պատրաստել ենք %s-ը, որպեսզի դուք կառավարեք այն, ինչ համօգտագործում եք առցանց և թե ինչով եք կիսվում մեզ հետ:Կարդացեք մեր գաղտնիության ծանուցումը
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index 07940e8e9..7644c30a0 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -1105,6 +1105,8 @@
Membebaskan ruang penyimpananIzin situs
+
+ UnduhanHapus data penjelajahan
@@ -1229,7 +1231,7 @@
Privasi Anda
- Kami merancang %s untuk memberi Anda kendali atas apa yang Anda bagikan secara daring dan apa yang Anda bagikan kepada kami.
+ Kami merancang %s agar Anda dapat mengendalikan apa saja yang Anda bagikan secara daring dan apa yang Anda bagikan kepada kami.Pelajari pemberitahuan privasi kami
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 98512e458..0c0d4252d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -130,6 +130,8 @@
Modifica segnalibroComponenti aggiuntivi
+
+ EstensioniNessun componente aggiuntivo
@@ -205,7 +207,7 @@
Adesso cerca con:
- Copia il link dagli appunti
+ Incolla il link dagli appuntiConsenti
@@ -540,6 +542,10 @@
Altri segnalibriCronologia
+
+ Nuova scheda
+
+ Trova nella paginaSchede sincronizzate
@@ -1123,6 +1129,8 @@
Libera spazio di archiviazionePermessi dei siti
+
+ DownloadElimina dati di navigazione
@@ -1247,7 +1255,7 @@
La tua privacy
- %s è progettato per darti il pieno controllo sulle informazioni che condividi online e con noi.
+ %s è progettato per darti il pieno controllo sulle informazioni che condividi online e con noi.Leggi la nostra informativa sulla privacy
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 07ddb3c38..1898fcb8b 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -130,6 +130,8 @@
עריכת סימנייהתוספות
+
+ הרחבותאין תוספות כאן
@@ -525,6 +527,10 @@
סימניות אחרותהיסטוריה
+
+ לשונית חדשה
+
+ חיפוש בדףלשוניות מסונכרנות
@@ -1049,7 +1055,7 @@
הקישור הועתק
- זהו טקסט לדוגמה. זה כאן כדי להראות כיצד טקסט יופיע בעת הגדלה או הקטנת הגודל עם הגדרה זו.
+ זהו טקסט לדוגמה. זה כאן כדי להראות כיצד טקסט יוצג בעת הגדלה או הקטנת הגודל עם הגדרה זו.הגדלה או הקטנה של טקסט באתרים
@@ -1086,6 +1092,8 @@
משחרר שטח אחסוןהרשאות אתר
+
+ הורדותמחיקת נתוני גלישה
@@ -1208,8 +1216,7 @@
- עיצבנו את %s כדי להעניק לך שליטה במה שמעניין אותך לשתף
- ברשת ומה שמעניין אותך לשתף איתנו.
+ עיצבנו את %s כדי להעניק לך שליטה במה שמעניין אותך לשתף ברשת ומה שמעניין אותך לשתף איתנו.קריאת הצהרת הפרטיות שלנו
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index abf77da2f..a757700b2 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -1107,6 +1107,8 @@
ストレージ領域を空けますサイトの許可設定
+
+ ダウンロード一覧閲覧データを削除
@@ -1229,7 +1231,7 @@
あなたのプライバシー
- %s は、あなたがオンラインで共有するものと、私たちと共有するものをコントロールできるように設計されています。
+ %s は、あなたがオンラインで共有するものと、私たちと共有するものをコントロールできるように設計されています。個人情報保護方針を読む
diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml
index 7e97edbb4..336ff332e 100644
--- a/app/src/main/res/values-ka/strings.xml
+++ b/app/src/main/res/values-ka/strings.xml
@@ -1086,6 +1086,8 @@
გაათავისუფლებს მეხსიერებასსაიტის ნებართვები
+
+ ჩამოტვირთვებიდათვალიერების მონაცემების წაშლა
@@ -1208,8 +1210,7 @@
თქვენი პირადულობა
- %s შექმნილია ისე, რომ თავად წყვეტდეთ რას გააზიარებთ
- ინტერნეტში და რას გაგვიზიარებთ ჩვენ.
+ %s შექმნილია ისე, რომ თავად წყვეტდეთ რას გააზიარებთ ინტერნეტში და რას გაგვიზიარებთ ჩვენ.გაეცანით პირადულობის განაცხადს
diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml
index b0ee815b1..7018fa864 100644
--- a/app/src/main/res/values-kab/strings.xml
+++ b/app/src/main/res/values-kab/strings.xml
@@ -133,6 +133,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara
Beddel tacreḍt n usebterIzegrar
+
+ IsiɣzafUlac izegrar da
@@ -535,6 +537,10 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara
Ticrad nniḍenAmazray
+
+ Iccer amaynut
+
+ Af deg usebterIccaren yemtawin
@@ -1101,6 +1107,8 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara
Tisirag n usmel
+
+ IsidarKkes isefka n tunigin
@@ -1223,9 +1231,7 @@ Tiktiwin tigejdanin yuzzlen ur nṣeḥḥi ara
- Nfeṣṣel %s akken ad nerr gar ifassen-ik ayen i tbeṭṭuḍ
- srid daɣen ayen i beṭṭuḍ yid-neɣ.
-
+ Nfeṣṣel %si w akken ad nerr gar ifassen-ik·im ayen i tbeṭṭuḍ srid aked wayen tbeṭṭuḍ yid-neɣ.Ɣer tasertit-nneɣ n tbaḍnit
diff --git a/app/src/main/res/values-kk/strings.xml b/app/src/main/res/values-kk/strings.xml
index 8dfc7188d..d7944dc43 100644
--- a/app/src/main/res/values-kk/strings.xml
+++ b/app/src/main/res/values-kk/strings.xml
@@ -1078,6 +1078,8 @@
Жинақтауыш орнын босатадыСайт рұқсаттары
+
+ ЖүктемелерШолу деректерін өшіру
@@ -1203,8 +1205,7 @@
Сіздің жекелігіңіз
- Біз %s өнімін сіз желіде және бізбен немен бөлісетіңізді басқаруды
- өз қолыңызға беретіндей етіп жасадық.
+ Біз %s өнімін сіз желіде және бізбен немен бөлісетіңізді басқаруды өз қолыңызға беретіндей етіп жасадық.Біздің жекелік ескертуімізді оқыңыз
diff --git a/app/src/main/res/values-kmr/strings.xml b/app/src/main/res/values-kmr/strings.xml
new file mode 100644
index 000000000..a883ba0e3
--- /dev/null
+++ b/app/src/main/res/values-kmr/strings.xml
@@ -0,0 +1,1694 @@
+
+
+
+ %s’a veşartî
+
+ %s (Veşartî)
+
+
+
+
+ Vebijarkên zêdetir
+
+ Gerîna veşartî veke
+
+ Gerîna veşartî bigire
+
+ Lêgerîn yan navnîşanek
+
+ Hilpekînên te yên vekirî ew ê li vir bên nîşandan.
+
+ Hilpekînên te yên veşartî ew ê li vir bên nîşandan.
+
+ Baidu
+
+ JD
+
+ 1 hilpekîna vekirî. Ji bo hilpekînê biguherînî, bitikîne.
+
+ %1$s hilpekînên vekirî. Ji bo hilpekînê biguherînî, bitikîne.
+
+ %1$d hilpekîn hatin bijartin
+
+ Koleksiyona nû tevlî bike
+
+ Nav
+
+ Koleksiyonê hilbijêre
+
+ Ji moda pir-bijartinê derkeve
+
+ Hilpekînên bijartî li koleksiyonê qeyd bike
+
+ %1$s hat bijartin
+
+
+ Bıjartina %1$s hate rakirin
+
+ Ji moda pir-bijartinê hate derketin
+
+
+ Tu ket moda pir-bijartinê, ji bo tomarkirina li koleksiyonê hilpekînan hilbijêre
+
+ Hate bijartin
+
+
+ %1$s berhemeke Mozillayê ye.
+
+
+
+ Tu di rûniştina veşartî de yî
+
+
+ %1$s, dema ku te hilpekînên veşartî girtin an jî tu ji sepanê derket ew ê raboriya te ya gerîn û lêgerînê ji hilpekînên veşartî bên paqijkirin. Ev, rê li ber şopandina/nedîtina te ya ji aliyê malper û peydakerên servîsê ve nagirê lê heke hin kesên din ên vê cîhazê bi kar tînin hebin ew ê bihêle ku tu raboriya xwe ji wan veşêrî.
+
+ Efsaneyên berbelav ên derbarê gerîna veşartî de
+
+
+ Rûniştinê jê bibe
+
+
+
+ Ji bo ku tu ji ekrana xwe ya Destpêkê hilpekîneke veşartî vekî, kurterêyekê tevlî bike.
+
+ Kurterêyê tevlî bike
+
+ Na, spas
+
+
+
+ Tu dikarî Firefoxê saz bike ku girêdanan bi otomatîkî di sepanan de veke.
+
+ Here sazkariyan
+
+ Bigire
+
+
+ Gihîna kamerayê hewce ye. Here sazkariyên Androidê ji beşa destûran, destûrê bide.
+
+ Here sazkariyan
+
+ Bigire
+
+
+ Hilpekînên vekirî yên ku di ser wan re roj, hefte an jî meh derbas bûne tu dikarî wan bixweber bidî girtin.
+
+ Vebijarkan bibîne
+
+ Bigire
+
+
+ Tu dikarî raxistina hilpekînên vekirî biguherînî. Here sazkariyan û ji bin "xuyabûna hilpekînê" gridê hilbijêre.
+
+ Here sazkariyan
+
+ Bigire
+
+
+
+
+ Hilpekîna nû
+
+ Hilpekîna veşartî ya nû
+
+ Malperên sereke
+
+
+
+ Hilpekînên vekirî
+
+ Paşve
+
+ Pêşve
+
+ Nû bike
+
+ Rawestîne
+
+ Favorî
+
+ Favoriyan serast bike
+
+ Pêvek
+
+ Ti pêvek li vir tune
+
+ Alîkarî
+
+ Çi tiştên nû hene?
+
+ Sazkarî
+
+ Pirtûkxane
+
+ Malpera sermaseyê
+
+ Tevlî ekrana destpêkê bike
+
+ Saz bike
+
+ Hilpekînên senkronîzekirî
+
+ Dîsa senkronîze bike
+
+ Di rûpelê de bibîne
+
+ Hilpekîna veşartî
+
+ Li koleksiyonê tomar bike
+
+ Parve bike
+
+ Parve bike bi…
+
+ Di %1$s de veke
+
+ Bihêzkirin ji aliyê %1$s
+
+ Bihêzkirin ji aliyê %1$s
+
+ Moda xwînerê
+
+ Moda xwînerê bigire
+
+ Di sepanê de veke
+
+ Xuyang
+
+ Nayê girêdan. Şemaya URLyê nayê nasîn.
+
+
+
+ Zimanê hilbijartî
+
+
+
+ Lêgerîn
+
+ Zimanê cîhazê
+
+ Li zimanan bigere
+
+
+
+ Sken
+
+ Motora lêgerînê
+
+ Sazkariyên motora lêgerînê
+
+ Îja ka bi vê bigere:
+
+ Girêdanê ji panoyê bîne
+
+ Destûrê bide
+
+ Destûrê nede
+
+ Di rûniştina veşartî de bila pêşniyarên lêgerînê werin pêş te?
+
+ %s dê hemû tiştên tu li darikê navnîşanan binivîsî bi motora te ya lêgerînê ya derbasdar re parve bike.
+
+ Zêdetir bizane
+
+
+
+ Hilpekîna nû ya Firefoxê veke
+
+ Lêgerîn
+
+ Di web’ê de bigere
+
+ Lêgerîna dengî
+
+
+
+ Sazkarî
+
+ Bingeh
+
+ Giştî
+
+ Derbar
+
+ Motora lêgerînê ya jixweber
+
+ Lêgerîn
+
+ Dariikê navnîşanan
+
+ Alîkarî
+
+ Li ser Google Playê puan bide
+
+ Paşragihandinê bişîne
+
+ Derbarê %1$s
+
+
+
+ Mafên te
+
+ Pêborîn
+
+ Kartên krediyê û navnîşan
+
+ Bike geroka sereke
+
+ Pêşketî
+
+ Nihênî
+
+ Nihênî û ewlehî
+
+ Destûrên malperê
+
+ Gerîna veşartî
+
+ Girêdanan di hilpekîna veşartî de veke
+
+ Destûrê bide girtina wêneyê ekranê di gera nepen de
+
+ Heke destûrê bidî, gava gelek bernamok vekirî bin hilpekînên nepen dê xuya bibin
+
+ Kurteriya gera nepen lê zêde bike
+
+ Gihînbarî
+
+ Servera taybet a hesabê Firefoxê
+
+ Servera taybet a Sync`ê
+
+ Hesabê Firefoxê/PÊşkêşkara Sync hat guhertin. Ji bo sepandina guhertinan ji bernameyê derdikeve…
+
+ Hesab
+
+ Têkeve
+
+ Darikê amûran
+
+ Rûkar
+
+ Serrûpel
+
+ Îşaretên tiliyan
+
+ Taybet bike
+
+ Bi hesabê xwe yê Firefoxê favorî, mêjû û zêdetir tiştên xwe hevseng bike
+
+ Hesabê Firefoxê
+
+ Ji bo dewamkirina hevsengkirinê dîsgirêdanê çêke
+
+ Ziman
+
+ Vebijarkên daneyê
+
+ Berhevkirina daneyan
+
+ Agahdariya nihêniyê
+
+ Amûrên pêşvebirinê
+
+ Bi riya USBê ji dûr ve neqandina çewtiyan
+
+ Motorên lêgerînê nîşan bide
+
+ Pêşniyarên lêgerînê nîşan bide
+
+ Lêgerîna dengî nîşan bide
+
+ Di rûniştinên veşartî de nîşan bide
+
+ Pêşniyarên panoyê nîşan bide
+
+ Di raboriya gerînê de lê bigere
+
+ Di favoriyan de bigere
+
+ Di hilpekînên hevsengkirî de bigere
+
+ Sazkariyên hesêb
+
+ Navnîşanan bixweber dagire
+
+ Girêdanan di sepanan de veke
+
+ Rêvebera jêbarkirinê ya derveyî
+
+ Pêvek
+
+ Danezan
+
+
+
+ Koleksiyona pêvekên taybet
+
+ Baş e
+
+ Betal bike
+
+ Navê koleksiyonê
+
+ Xwediyê koleksiyonê (nasnameya bikarhêner)
+
+
+ Koleksiyona pêvekan hat guhertin. Ji bo sepandina guhertinan ji bernamokê derdikeve…
+
+
+
+ Pêvek nayê piştgirîkirin
+
+
+ Pêvek jixwe sazkirî ye
+
+
+
+
+ Aniha senkronîze bike
+
+ Bila çi bên senkronîzekirin, hilbijêre
+
+ Raborî
+
+ Favorî
+
+ Hesab
+
+ Hilpekînên vekirî
+
+ Derkeve
+
+ Navê cîhazê
+
+ Navê cîhazê nabe vala bimîne.
+
+
+ Tê sekronîzekirin…
+
+
+ Senkronîzekirin têk çû, Senkronîzeya dawî: %s
+
+ Senkronîzekirin têk çû, Senkronîzeya dawî: tune
+
+ Senkronîzekirina dawî: %s
+
+ Senkronîzekirina dawî: tune
+
+
+ %1$s - %2$s %3$s
+
+
+
+ Hilpekînên hatine wergirtin
+
+ Danezanên ji bo hilpekînên ji amûrên din yên Firefoxê hatî
+
+ Hilpekîn hate wergirtin
+
+ Hilpekîn hatin wergirtin
+
+ Hilpekîna ji %s ve hat
+
+
+
+ Parastina ji Şopandinê
+
+ Parastina ji Şopandinê
+
+
+ Naverok û skrîptên li ser înternetê te dişopînin asteng bike
+
+ Istisna
+
+ Ji bo van malperan Parastina ji Şopandinê hat girtin
+
+ Ji bo hemû malperan veke
+
+ Istisna dihêle ku tu dikaribî ji bo malperên hilbijartî parastina şopandinê bigirî.
+
+ Zêdetir bizane
+
+
+ Bi temamî hat girtin, ji bo vekirinê here Eyaran.
+
+
+ Telemetrî
+
+ Daneyên bikaranînê û teknîkî
+
+ Daneyên bazarkirinê
+
+ Lêkolîn
+
+
+ Destûrê bide Mozillayê ku lêkolînan saz bike û bimeşîne
+
+ Ceribandin
+
+ Destûrê dide Mozillayê ku ji bo taybetiyên ceribandinê daneyan saz bike û berhev bike
+
+ Raporkera têkçûnan
+
+
+ Xizmeta cîgehê ya Mozillayê
+
+ Rapora tenduristiyê ya %s’ê
+
+
+
+ Sync’ê veke
+
+ Koda hevsengkirinê ya di Firefoxa Sermaseyê de sken bike
+
+ Têkeve
+
+ Ji bo dîsberibandinê têkevinê
+
+ Hesêb rake
+
+
+
+ firefox.com/pair de xuya dibe sken bike]]>
+
+ Kamerayê veke
+
+ Betal
+
+
+
+ Li jorê
+
+ Li jêrê
+
+
+
+ Ronî
+
+ Tarî
+
+
+ Li gorî teserufa akuyê
+
+ Rûkara cîhazê bi kar bîne
+
+
+
+ Ji bo nûkirinê bikişîne
+
+ Ji bo veşartina derikê amûran bişemitîne
+
+ Ji bo guhertina hilpekînê darikê amûran bi kêlekê ve kaş bike
+
+ Ji bo vekirina hilpekînan darikê amûran bi jorê ve bikişîne
+
+
+
+ Rûniştin
+
+ Wêneyên ekranê
+
+ Daxistinên te
+
+ Favorî
+
+ Favoriyên Sermaseyê
+
+ Menuya Favoriyan
+
+ Darikê Amûran yê Favoriyan
+
+ Favoriyên din
+
+ Raborî
+
+ Hilpekînên hevsengkirî
+
+ Lîsteya xwendinê
+
+ Lêgerîn
+
+ Sazkarî
+
+ Menûya hêmana raboriyê
+
+ Bigire
+
+
+ Hilpekînên dawiyê hatî girtin
+
+ Mêjûyê hemû nîşan bide
+
+ %d hilpekîn
+
+ %d hilpekîn
+
+
+ Hilpekîna di nêzîk de hatî girtin nîn in
+
+
+
+ Hilpekîn
+
+ Xuyabûna hilpekînê
+
+ Lîste
+
+ Grid
+
+
+ Hilpekînan bigire
+
+ Manûel
+
+ Piştî rojekê
+
+ Piştî hefteyekê
+
+ Piştî mehekê
+
+
+ Manûelê bigire
+
+ Piştî rojekê bigire
+
+ Piştî hefteyekê bigire
+
+ Piştî mehekê bigire
+
+
+
+ Hilpekînên vekirî
+
+ Rûniştina veşartî
+
+ Hilpekînên veşartî
+
+ Hilpekînê tevlî bike
+
+ Hilpekîna veşartî tevlî bike
+
+ Veşartî
+
+ Hilpekînên vekirî
+
+ Li koleksiyonê tomar bike
+
+ Hilbijêre
+
+ Hemû hilpekînan parve bike
+
+ Hilpekînên dawiyê hatî girtin
+
+ Sazkariyên hilpekînê
+
+ Hemû hilpekînan bigire
+
+ Hilpekîna nû
+
+ Here serrûpelê
+
+ Moda hilpekînan biguherîne
+
+ Favorî
+
+ Bigire
+
+ Hilpekînên hilbijartî parve bike
+
+ Menuya hilpekînên hilbijartî
+
+ Hilpekînê ji koleksiyê rake
+
+ Hilpekînan hilbijêre
+
+ Hilpekînê bigire
+
+ Hilpekîna %s’ê bigire
+
+ Menûya hilpekînên vekirî, veke
+
+ Hemû hilpekînan bigire
+
+ Hilpekînan parve bike
+
+ Hilpekînan li koleksiyonê tomar bike
+
+ Menûya hilpekînan
+
+ Hilpekînê parve bike
+
+ Jê bibe
+
+ Tomar bike
+
+ Parve bike
+
+ Wêneyê rûniştina niha
+
+ Li koleksiyonê tomar bike
+
+ Koleksiyonê jê bibe
+
+ Koleksiyonê bi nav bike
+
+ Hilpekînan veke
+
+ Navê koleksiyonê
+
+ Nav biguherîne
+
+ Rake
+
+ Ji raboriyê rake
+
+ %1$s (Moda Veşartî)
+
+ Tomar bike
+
+
+
+ Raboriyê jê bibe
+
+ Tu bi rastî jî dixwazî raboriya xwe paqij bikî?
+
+ Raborî hate jêbirin
+
+
+
+ %1$s hate jêbirin
+
+ Paqij bike
+
+ Kopî
+
+ Parve bike
+
+ Di hilpekîna nû de veke
+
+ Di hilpekîna veşartî de veke
+
+ Jê bibe
+
+ %1$d hate bijartin
+
+ %1$d hêmanan jê bibe
+
+ Îro
+
+ Doh
+
+ 24 saetên dawî
+
+ 7 rojên dawî
+
+ 30 rojên dawî
+
+ Kevntir
+
+ Raborî tune
+
+
+
+ Daxistinan jê bibe
+
+ Tu bi rastî jî dixwazî daxistinên xwe paqij bikî?
+
+ Daxistin hatin rakirin
+
+ %1$s hate rakirin
+
+ Ti tişt nehatiye daxistin
+
+ %1$d hatin bijartin
+
+ Veke
+
+ Rake
+
+
+
+
+ Bibore. %1$s, nikare vê rûpelê bar bike.
+
+ Tu dikarî biceribînî ku vê hilpekînê bifilitînî yan jî bigirî.
+
+ Rapora têkçûnê ji Mozillayê re bişîne
+
+ Hilpekînê bigire
+
+
+ Hilpekînê vegerîne
+
+
+ Vebijarkên rûniştinê
+
+
+ Rûniştinê parve bike
+
+
+
+ Menuya Favoriyan
+
+ Favoriyan serast bike
+
+ Peldankê hilbijêre
+
+ Tu bi rastî jî dixwazî vê peldankê jê bibî?
+
+ %s ew ê hêmanên bijartî jê bibe.
+
+ %1$s hate jêbirin
+
+ Peldankê tevlî bike
+
+ Favorî hat afirandin.
+
+ Favorî hat hilanîn!
+
+ SERERAST BIKE
+
+ Sererast bike
+
+ Hilbijêre
+
+ Kopî bike
+
+ Parve bike
+
+ Di hilpekîna nû de veke
+
+ Di hilpekîna veşartî de veke
+
+ Jê bibe
+
+ Tomar bike
+
+
+ %1$d hatin bijartin
+
+ Favoriyan serast bike
+
+ Peldankê sererast bike
+
+ Ji bo favoriyan hevsengirî bibînî têkevê
+
+ NAVNÎŞAN
+
+ PELDANK
+
+ NAV
+
+ Peldankê tevlî bike
+
+ Peldankê hilbijêre
+
+ Sernav hewce ye
+
+
+ URL’ya nederbasdar
+
+ Hîç favorî nîn in
+
+ %1$s hate jêbirin
+
+ Favorî hat jêbirin
+
+ Peldankên hilbijartî tên jêbirin
+
+ VEGERÎNE
+
+
+
+ Destûr
+
+ Here sazkariyan
+
+ Rûpela sazkariyên zû
+
+ Tê pêşniyarkirin
+
+ Destûrên malperê birêve bibe
+
+ Destûran paqij bike
+
+ Destûrê paqij bike
+
+ Destûran ji hemû malperan paqij bike
+
+ Lêdera xweber
+
+ Kamera
+
+ Mîkrofon
+
+ Cîgeh
+
+ Danezan
+
+ Bîrgeha mayinde
+
+ Naveroka DRM kontrolkirî
+
+ Destûrê bixwaze
+
+ Hate astengkirin
+
+ Destûrgirtî
+
+ Ji hêla Androidê ve hate astengkirin
+
+ Istisna
+
+ Vekirî
+
+ Girtî
+
+ Destûrê bide deng û vîdyoyê
+
+ Deng û vîdyoyê tenê di daneya hucreyî de asteng bike
+
+ Deng û vîdyo ew ê tenê di Wi-Fi de bên vekirin
+
+ Tenê dengan asteng bike
+
+ Deng û vîdyoyê asteng bike
+
+ Vekirî
+
+ Girtî
+
+
+
+ Koleksiyon
+
+ Menûya koleksiyonê
+
+ Tiştên ji te re girîng berhev bike.\nLêgerîn, malper û hilpekînên dixwazî xwe zû bigihînî wan bîne cem hev.
+
+ Hilpekînan hilbijêre
+
+ Koleksiyonê hilbijêre
+
+ Koleksiyonê bi nav bike
+
+ Koleksiyona nû tevlî bike
+
+ Gişî hilbijêre
+
+ Hemû hilbijartinan rake
+
+ Ji bo tomarkirinê, hilpekînan hilbijêre
+
+ %d hilpekîn hatin bijartin
+
+ %d hilpekîn hate bijartin
+
+ Hilpekîn hatin tomarkirin!
+
+ Koleksiyon hate tomarkirin!
+
+ Hilpekîn hate tomarkirin!
+
+ Bigire
+
+ Tomar bike
+
+ Bibîne
+
+
+ Koleksiyona %d
+
+
+
+ Bişîne û parve bike
+
+ Parve bike
+
+ Parve bike
+
+ Girêdanê parve bike
+
+ Bişîne cîhazê
+
+ Hemû çalakî
+
+ Dawiyê hatî bikaranîn
+
+ Têkeve Sync’ê
+
+ Ji hemû cîhazan re bişîne
+
+ Dîsa li Sync’ê girê bide
+
+ Derhêl
+
+ Cîhazeke din girê bide
+
+ Ji bo şandina hilpekînekê herî kêm ji amûreke din têkeve Firefoxê.
+
+ Min fêm kir
+
+ Bi vê amûrê re nayê parvekirin
+
+ Bişîne cîhazê
+
+
+ Amûra girêdayî nîn e
+
+
+ Derbarê Şandina Hilpekînan de agahiyan bistîne…
+
+ Bi amûreke din ve girê bide…
+
+
+
+ Danişîna gera nepen
+
+ Hilpekînên nepen jê bibe
+
+ Hilpekînên nepen bigire
+
+ Veke
+
+ Jê bibe û veke
+
+ Bihêzkirin ji aliyê
+
+ Koleksiyon hate jêbirin
+
+ Navê koleksiyê hat guhertin
+
+ Hilpekîn hate jêbirin
+
+ Hilpekîn hatin jêbirin
+
+ Hilpekîn hate girtin
+
+ Hilpekîn hatin girtin
+
+ Hilpekîn hatin girtin!
+
+ Favorî hatin tomarkirin!
+
+ Bibîne
+
+ Tevlî malperên sereke bû!
+
+ Hilpekîna veşartî hate girtin
+
+ Hilpekînên veşartî hatin girtin
+
+ Hilpekînên veşartî hatin jêbirin
+
+ VEGERÎNE
+
+ Malper hate rakirin
+
+ Vegerîne
+
+ Bipejirîne
+
+ Destûrê bide %1$sê ku %2$sê veke
+
+ BIHÊLE
+
+ NEHÊLE
+
+ Tu bi rastî jî dixwazî koleksiyana %1$s’ê were jêbirin?
+
+ Jêbirina vê hilpekînê dê hemû koleksiyê jê bibe. Kengê bixwazî dikarî koleksiyoneke nû biafirînî.
+
+ %1$s’ê jê bibe?
+
+ Jê bibe
+
+ Betal bike
+
+ Têketina moda ekrana dagirtî
+
+ URL hate kopîkirin
+
+ Ev nivîseke nimûneyî ye. Li virê xuya dibe ka gava tu bi vê sazkariyê mezinahiyê biguherînî dê çawa be.
+
+ Nivîsên li ser malperan mezintir yan biçûktir bike
+
+ Mezinahiya fontê
+
+
+ Lêaîna mezinahiya nivîsan ya xweber
+
+ Mezinahiya nivîsan dê li gorî sazkariyên Androîda te be. Ji bo mezinahiyê nivîsê li virê biguherînî vê wê betal bike.
+
+
+ Daneyên gerînê jê bibe
+
+ Hilpekînên vekirî
+
+ %d hilpekîn
+
+ Raboriya gerînê û daneyên malperê
+
+ %d navnîşan
+
+ Raborî
+
+ %d rûpel
+
+ Kûkî
+
+ Danişînên li ser gelek malperan dê bên girtin
+
+ Wêne û pelên di pêşbîrê de
+
+ Ciyê depokirinê vala dike
+
+ Destûrên malperan
+
+ Daxistinên te
+
+ Daneyên gerînê jê bibe
+
+ Daneyên gerê di dema derketinê de jê bibe
+
+ Gava tu ji menuya giştî "Derkeve"yê hilbijêrî daneyên gerê xweber paqij dike
+
+ Gava tu ji menuya giştî \"Derkeve\"yê hilbijêrî daneyên gerê xweber paqij dike
+
+ Derkeve
+
+
+ Ev ê hemû daneyên te yên gerê jê bibe
+
+ %s dê daneyên gerê yên hilbijartî jê bibe.
+
+ Betal bike
+
+ Jê bibe
+
+
+ Daneyên gerînê hate jêbirin
+
+ Daneyên gerînê tên jêbirin…
+
+
+
+ Firefox Preview niha bûye Firefox Nightly
+
+
+
+ Firefox Nightly her şev tê nûkirin û tê de taybetiyên nû yên ceribandinê hene.
+ Lê belê, dibe ku hindiktir stabîl be. Ji bo ceribandineke stabîltir geroka me ya beta jêbar bike.
+
+ Firefox Betaya ji bo Andoîdê jêbar bikin
+
+
+ Firefox Nightlyê bar kir
+
+
+ Ev sepan dê venûkirinên ewlekariyê êdî wernegire. Êdî vê sepanê bi kar neyne û derbasî Nightlya nû bibe.
+ \ n \ nJi bo transferkirina favorî, têketin û mêjûya xwe bo sepaneke din, hesabekî Firefoxê biafirîne.
+
+ Derbasî Nightlya nû bibe
+
+
+ Firefox Nightlyê bar kir
+
+
+ Ev sepan dê venûkirinên ewlekariyê êdî wernegire. Êdî vê sepanê bi kar neyne û derbasî Nightlya nû bibe.
+ \ n \ nJi bo transferkirina favorî, têketin û mêjûya xwe bo sepaneke din, hesabekî Firefoxê biafirîne.
+
+ Nightlya nû jêbar bike
+
+
+
+ Tu bi xêr hatî %s’ê!
+
+ Jixwe hesabekî te heye?
+
+ %s’ê nas bike
+
+ Tiştên nû bibîne
+
+ Pirsên te derbarê %sa nû de hene? Dixwazî bizanî ka çi guheriye?
+
+ Bersiv li vir in
+
+ Bi hesabê xwe yê Firefoxê dest bi hevsengkirina favoriyan, şîfreyan û zêdetir tiştên xwe bike.
+
+ Zêdetir bizane
+
+ Te li ser vê amûrê wekî %s li ser gerokeke din a Firefoxê têketin pêk anî. Tu dixwazî bi vî hesabî têkeviyê?
+
+ Erê, têkeve
+
+ Dikevê…
+
+ Têkeve Firefox’ê
+
+ Têketinê neke
+
+ Sync vekirî ye
+
+ Têketin bi ser neket
+
+ Nihêniya otomatîk
+
+ Sazkariyên nepeniyê û ewlekariyê şopîneran û sepanên niyetxirab û şirketên dixwazin te bişopînin asteng dike.
+
+ Standard (jixweber)
+
+ Hindiktir şopîneran asteng dike. Rûpel dê bi awayê asayî bên barkirin.
+
+ Tund (tê pêşniyarkirin)
+
+ Tund
+
+ Zêdetir şopîneran, reklaman û popupan asteng dike. Rûpel zûtir vedibin, lê dibe ku hin taybetî nexebitin.
+
+ Aliyê xwe hilbijêre
+
+ Bi darikê amûran yê li xwarê gera bi destekî biceribîne yan jî wê bibe jorê.
+
+ Bi nepenî bigere
+
+ Carekê hilpekîneke nepen veke: Pêl îkona %sê bike.
+
+ Her car hilpekînên nepen veke: Sazkariyên xwe yên gera nepen venû bike.
+
+ Sazkariyan veke
+
+ Nihêniya te
+
+ Me %s ji bo hindê çêkiriye ku derbarê tiştên tu li ser înternetê û tiştên bi me re parve dikî de kontrol di destê te de be.
+
+ Agahdariya me ya nihêniyê bixwîne
+
+ Bigire
+
+
+ Dest bi gerînê bike
+
+
+
+ Rûkara xwe hilbijêre
+
+ Bi çalakkirina moda tarî him ji bataryayê teseruf bike him çavên xwe vehesîne.
+
+ Otomatîk
+
+ Xwe li sazkariyên amûra te tîne
+
+ Rûkara tarî
+
+ Rûkara ronî
+
+
+ Hilpekîn hatin şandin!
+
+ Hilpekîn hate şandin!
+
+ Nehate şandin
+
+ DÎSA BICERIBÎNE
+
+ Kodê sken bike
+
+ https://firefox.com/pair]]>
+
+ Ji bo skenkirinê amade me
+
+ Bi kameraya xwe têkevê
+
+ Bi e-peyamê têkevê
+
+ Yekê biafirîne ku Firefoxê di navbera amûran de hevseng bikî.]]>
+
+ Firefox dê hevsengkirina bi hesabê te re asteng bike, lê dê daneyên te yên gerê yên li ser vê amûrê jê nebe.
+
+ %s dê hevsengkirina bi hesabê te re asteng bike, lê dê daneyên te yên gerê yên li ser vê amûrê jê nebe.
+
+ Girêdanê qut bike
+
+ Betal bike
+
+
+ Peldankên standard nayên serastkirin
+
+
+
+ Sazkariyên parastinê
+
+ Parastina şopandinê ya pêşketî
+
+ Bêyî ku bêtî şopandin bigere
+
+ Bila daneyên te ji te re bimînin. %s te ji piraniya şopîneran diparêze ku tiştên tu li ser înternetê dikî dişopînin.
+
+ Zêdetir bizane
+
+ Standard (heyî)
+
+ Hindiktir şopîneran asteng bike. Rûpel dê asayî vebin.
+
+ Bi parastina şopandinê ya standard çi tên astengkirin?
+
+ Tund
+
+ Zêdetir şopîneran, reklaman û popupan asteng dike. Rûpel zûtir tên barkirin, lê dibe ku hin taybetî nexebitin.
+
+ Bi parastina şopandinê ya tund çi tên astengkirin?
+
+ Taybet
+
+ Hilbijêre ka kîjan şopîner û skrîpt bên astengkirin.
+
+ Bi parastina şopandina taybet çi tên astengkirin?
+
+
+ Kûkî
+
+ Şopînerên nav-malperî û şopînerên medyaya civakî
+
+ Çerezên malperên nehatine bikaranîn
+
+ Hemû çerezên partiya sêyemîn (dibe ku hin malper xira bibin)
+
+ Hemû çerezan (dibe ku hin malper xira bibin)
+
+ Naverokên şopandinê
+
+ Di hemû hilpekînan de
+
+ Tenê di hilpekînên veşartî de
+
+ Tenê di hilpekînên taybet de
+
+ Kankerên krîpto
+
+ Berhevkarên şoptiliyan
+ Astengker
+
+ Destûrgirtî
+
+ Şopînerên medyaya civakî
+
+ Taybetiya şopandina çalakiyên te yên gera li ser torên civakî bisînor dike.
+
+ Çerezên şopandinê yên nav-malperî
+
+ Çerezên ku torên reklamê û şirketên analîtîkê ji bo şopandina gera di navbera malperên cuda de bi kar tînin asteng dike.
+
+ Kankerên krîpto
+
+ Skrîptên xirab yên ji bo ku kankerên krîpto ji bo çêkirina pereyên krîpto xwe bigihînin amûra te asteng dike.
+
+ Şoptilî
+
+ Naverokên şopandinê
+
+ Barkirina reklam, vidyo û naverokên din yên tê de koda şopandin heye asteng dike. Dibe ku bandorê li taybetiyên hin malperan bike.
+
+ Gava mertal bû rengê mor, nexwe %sê şopîner asteng kirine. Ji bo agahiyên zêdetir bipelîne.
+
+ Parastin ji bo v malperê VEKIRÎ ne.
+
+ Parastin ji bo vê malperê GIRTÎ ne
+
+ Parastina şopandinê ya pêşketî ji bo van malperan hat girtin
+
+ Paşve here
+
+ Mafên te
+
+ Pirtûkxaneyên çavkaniya azad yên em bi kar tînin
+
+ Di %s çi tiştên nû hene?
+
+ %s | Pirtûkxaneyên OSS’ê
+
+
+ Şoînerên beralîkirinê
+
+ Çerezên aîdî beralîkirinên ji bo malperên şopandinê yên naskirî asteng dike.
+
+
+ Piştgirî
+
+ Têkçûn
+
+ Agahdariya nihêniyê
+
+
+ Mafên xwe bizane
+
+ Agahiyên lîsanskirinê
+
+ Pirtûkxaneyên em bikar tînin
+
+ Menuya neqandina çewtiyan: Ji bo çalakkirinê %1$d tikandin man
+
+ Menuya neqandina çewtiyan hat çalakkirin
+
+
+ 1 hilpekîn
+
+ %d hilpekîn
+
+
+
+ Kopî bike
+
+ Bizeliqîne û here
+
+ Bizeliqîne
+
+ URL li panoyê hate kopîkirin
+
+
+ Tevlî ekrana destpêkê bike
+
+ Betal bike
+
+ Tevlî bike
+
+ Li malperê berdewam bike
+
+ Navê kurterêyê
+
+
+ Tu dikarî vê malperê bi hêsanî li ekrana Serûpela amûra xwe zêde bikî ku zû bigihiyê û wê wekî sepanekê bi kar bînî.
+
+
+ Hesab û pêborîn
+
+ Hesab û pêborînan tomar bike
+
+ Ji bo tomarkirinê, bipirse
+
+ Qet tomar neke
+
+ Bixweber dagire
+
+ Hesaban senkronîze bike
+
+ Vekirî
+
+ Girtî
+
+ Dîsa girê bide
+
+ Têkeve Sync’ê
+
+ Hesabên tomarkirî
+
+ Têketinên te tomar kirine yan hevsengkirinên bi %sê re dê li virê bên nîşandan.
+
+ Derbarê Sync’ê de agahî bistîne.
+
+ Istisna
+
+ Têketin û şîfreyên nehatine tomarkirin dê li virê bên nîşandan.
+
+ Têketin û şîfre dê ji bo van malperan neyên tomarkirin.
+
+ Hemû istisnayan jê bibe
+
+ Li hesaban bigere
+
+ Alfabetîkî
+
+ Dawiyê hatî bikaranîn
+
+ Malper
+
+ Navê bikarhêner
+
+ Pêborîn
+
+ PIN’a xwe dîsa binivîse
+
+ Ji bo têketinên xwe yên tomarkirî bibînî kilîdê veke
+
+ Ev girêdan ne ewle ye. Îhtîmal heye têketinên li virê bên bidestxistin.
+
+ Zêdetir bizane
+
+ Bila %s, vî hesabî tomar bike?
+
+ Tomar bike
+
+ Tomar neke
+
+ Pêborîn li panoyê hate kopîkirin
+
+ Navê bikarhêner li panoyê hate kopîkirin
+
+ Malper li panoyê hate kopîkirin
+
+ Pêborînê kopî bike
+
+ Pêborînê paqij bike
+
+ Navê bikarhêner kopî bike
+
+ Navê bikarhêner paqij bike
+
+ Malperê kopî bike
+
+ Malperê di gerokê de veke
+
+ Pêborînê nîşan bide
+
+ Pêborînê veşêre
+
+ ji bo hesabên xwe yên tomarkirî bibînî, kilîdê veke
+
+ Hesab û pêborînên xwe biparêze
+
+ Piştre
+
+ Aniha saz bike
+
+ Kilîda cîhaza xwe rake
+
+ Hemû malperan nêzîk bike
+
+ Nav (A-Z)
+
+ Bikaranîna dawî
+
+
+ Menuya rêzkirina têketinan
+
+
+ Motora lêgerînê tevlî bike
+
+ Motora lêgerînê sererast bike
+
+
+ Tevlî bike
+
+ Tomar bike
+
+ Sererast bike
+
+ Jê bibe
+
+
+ Yên din
+
+ Nav
+
+ Rêzeya lêgerînê ya ji bo bikaranînê
+
+ Daxwazê bi “%s”. Mînak:\nhttps://www.google.com/search?q=%sê biguherîne
+
+ Zêdetir Bizane
+
+
+ Hûrgiliyên motora lêgerînê ya taybet
+
+ Lînka agahiyên zêdetir
+
+
+ Navê motora lêgerînê binivîse
+
+ Motora lêgerînê ya bi navê “%s” jixwe heye.
+
+ Rêzikeek lêgerînê binivîse
+
+ Kontrol bike ka rêzika lêgerînê û formata Mînak li hev dikin yan na
+
+ Êewtiya girêdana bi “%s”ê ve
+
+ %s hat afirandin
+
+ %s hat tomarkirin
+
+ %s hat jêbirin
+
+
+ Bi xêr hatî geroka %sê ya nipînû
+
+ Me geroka te bi performans û taybetiyên pêşxistî ji serî heta binî nû kir ku tu dikarî li ser înternetê tiştên zêdetir bikî.\n\nTika ye heta %sê van venû bike li bendê bin...
+
+ %s tê nûvekirin…
+
+ %sê bide destpêkirin
+
+ Veguhestin qediya
+
+ Pêborîn
+
+
+ Ji bo destûrdayînê:
+
+ 1. Here sazkariyên Androidê
+
+ Destûrê bitikîne]]>
+
+ %1$sê bike VEKIRÎ]]>
+
+
+ Girêdana ewle
+
+ Girêdana neewle
+
+ Tu ji dil dizanî hemû destûrên li ser hemû malperan jê bibî?
+
+ Tu ji dil dizanî hemû destûran ji bo hemû malperan jê bibî?
+
+ Tu ji dil dixwazî vê destûrê ji bo vê malperê jê bibî?
+
+ Istisnayên malperê tune
+
+ Gotarên Serekî
+
+ Tu ji dil dixwazî vê favoriyê jê bibî?
+
+ Tevlî malperên sereke bike
+
+ Piştrastkirin ji aliyê: %1$s
+
+ Jê bibe
+
+ Sererast bike
+
+
+ Tu bi rastî jî dixwazî vî hesabî jê bibî?
+
+ Jê bibe
+
+ Vebijarkên hesêb
+
+ Qada nivîsê ya ji bo navnîşana webê ya têketinê
+
+ Qada nivîsê ya ji bo navê bikarhêneriyê ya têketinê.
+
+ Qada nivîsê ya ji bo şîfreya têketinê.
+
+ Guhertinan li hesêb tomar bike.
+
+ Guhertinan betal bike
+
+ Sererast bike
+
+ Pêborîn hewce ye
+
+ Lêgerîna dengî
+
+ Aniha bipeyive
+
+ Jixwe hesabek bi vî navê bikarhêneriyê heye
+
+
+
+ Cîhazeke din girê bide.
+
+ Ji kerema xwe, ji nû ve têkeve.
+
+ Ji kerema xwe, senkronîzekirina hilpekînê veke.
+
+ Li ser amûrên din ti hilpekîneke di Firefoxê de vekirî tune ye.
+
+ Lîsteyeke hilpekînan ya ji amûrên xwe yên din bibîne.
+
+ Têkeve Sync’ê
+
+ Hilpekînên vekirî tune
+
+
+
+ Gihîşte sînorê malperên sereke
+
+
+ Ji bo malpereke din a sereke lê zêde bikî, yekê jê bibe. Li malperê bitikîne û li ser bihêle piştre rakirinê hilbijêre.
+
+ Baş e
+
+ Malperên herî zêde hatine vekirin nîşan bide
+
+ Nav
+
+ Navê malpera favorî
+
+ Baş e
+
+ Betal bike
+
+
+ Rake
+
+
+ Ji %sa xwe fêdeya herî zêde bibînin.
+
+
+ Ji bo agahiyên zêdetir, bitikîne
+
+
+ Tiştên ku ji te re girîng in berhev bike
+
+ Lêgerîn, malper û hilpekînên wekî hev bi hev re bike kom da ku paşê xwe zû bigihîniyê.
+
+ Tu li ser vê telefonê di gerokeke din ya Firefoxê de wekî %s têketiyê. Tu dixwazî bi vî hesabî têkeviyê?
+
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 811ae4603..33f8833f0 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -137,6 +137,8 @@
북마크 편집부가 기능
+
+ 확장 기능부가 기능 없음
@@ -542,6 +544,10 @@
다른 북마크기록
+
+ 새 탭
+
+ 페이지에서 찾기동기화된 탭
@@ -1132,6 +1138,8 @@
저장 공간 확보사이트 권한
+
+ 다운로드탐색 데이터 삭제
@@ -1256,10 +1264,7 @@
개인 정보 보호
- 우리는 %s가 무엇을 온라인에서 공유하고 우리와 공유할지
- 사용자가 제어할 수 있게 만들었습니다.
-
-
+ 우리는 %s가 무엇을 온라인에서 공유하고 우리와 공유할지 사용자가 제어할 수 있게 만들었습니다.개인정보처리방침 읽기
diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml
index 294d1cd32..7a58543c2 100644
--- a/app/src/main/res/values-lt/strings.xml
+++ b/app/src/main/res/values-lt/strings.xml
@@ -129,6 +129,8 @@
Redaguoti adresyno įrašąPriedai
+
+ PriedaiČia nėra jokių priedų
@@ -529,6 +531,10 @@
Kiti adresaiŽurnalas
+
+ Nauja kortelė
+
+ Rasti tinklalapyjeSinchronizuotos kortelės
@@ -1091,6 +1097,8 @@
Atlaisvina vietos įrenginyjeSvetainių leidimai
+
+ AtsiuntimaiPašalinti naršymo duomenis
@@ -1215,7 +1223,7 @@
Jūsų privatumas
- Mes sukūrėme „%s“ siekdami jums suteikti pasirinkimą, kuo dalinatės internete, ir kuo su mumis.
+ Mes sukūrėme „%s“ siekdami jums suteikti pasirinkimą, kuo dalinatės internete, ir kuo su mumis.Mūsų privatumo pranešimas
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index 6bb2f8b04..21b4ab79d 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -132,6 +132,8 @@
Rediger bokmerkeTillegg
+
+ UtvidelserIngen utvidelser her
@@ -535,6 +537,10 @@
Andre bokmerkerHistorikk
+
+ Ny fane
+
+ Søk på sidenSynkroniserte faner
@@ -1098,6 +1104,8 @@
Nettstedstillatelser
+
+ NedlastingerSlett nettleserdata
@@ -1226,8 +1234,7 @@
- Vi har utviklet %s for å gi deg kontroll over det du deler
- på nettet og hva du deler med oss.
+ Vi har utviklet %s for å gi deg kontroll over det du deler på nettet og hva du deler med oss.Les personvernmerknaden vår
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
index 72dd2a61c..be4f0f66a 100644
--- a/app/src/main/res/values-night/colors.xml
+++ b/app/src/main/res/values-night/colors.xml
@@ -52,6 +52,7 @@
@color/search_suggestion_indicator_icon_bookmark_color_dark_theme@color/accent_high_contrast_dark_theme@color/preference_section_header_dark_theme
+ @color/accent_high_contrast_dark_theme@color/mozac_widget_favicon_background_dark_theme@color/mozac_widget_favicon_border_dark_theme
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index d4619ce04..c946b5080 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -135,6 +135,8 @@
Bladwijzer bewerkenAdd-ons
+
+ ExtensiesGeen add-ons hier
@@ -533,6 +535,10 @@
Andere bladwijzersGeschiedenis
+
+ Nieuw tabblad
+
+ Zoeken op paginaGesynchroniseerde tabbladen
@@ -1097,6 +1103,8 @@
Maakt opslagruimte vrijWebsitemachtigingen
+
+ DownloadsNavigatiegegevens verwijderen
@@ -1127,8 +1135,9 @@
Firefox Preview is nu Firefox Nightly
- Firefox Nightly wordt elke nacht bijgewerkt en heeft experimentele nieuwe functies.
- Het is echter mogelijk minder stabiel. Download onze bètabrowser voor een meer stabiele ervaring.
+
+ Firefox Nightly wordt elke nacht bijgewerkt en heeft experimentele nieuwe functies.
+ Het is echter mogelijk minder stabiel. Download onze betabrowser voor een meer stabiele ervaring.Download Firefox voor Android Beta
@@ -1219,11 +1228,7 @@
Uw privacy
- We hebben %s ontworpen om u controle te geven over wat u online
-
- deelt en wat u met ons deelt.
-
-
+ We hebben %s ontworpen om u controle te geven over wat u online deelt en wat u met ons deelt.Lees onze privacyverklaring
diff --git a/app/src/main/res/values-nn-rNO/strings.xml b/app/src/main/res/values-nn-rNO/strings.xml
index 58d1377c7..0c50a0747 100644
--- a/app/src/main/res/values-nn-rNO/strings.xml
+++ b/app/src/main/res/values-nn-rNO/strings.xml
@@ -133,6 +133,8 @@
Rediger bokmerkeTillegg
+
+ UtvidingarIngen tillegg her
@@ -533,6 +535,10 @@
Andre bokmerkeHistorikk
+
+ Ny fane
+
+ Finn på sidaSynkroniserte faner
@@ -1101,6 +1107,8 @@
Frigjer lagringsplassNettstadløyve
+
+ NedlastingarSlett nettlesardata
@@ -1225,8 +1233,7 @@
Ditt personvern
- Vi har utvikla %s for å gi deg kontroll over det du deler
- på nettet og kva du deler med oss.
+ Vi har utvikla %s for å gi deg kontroll over det du deler på nettet og kva du deler med oss.Les personvernmerknaden vår
diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml
index 94fd82fa3..dab397e11 100644
--- a/app/src/main/res/values-oc/strings.xml
+++ b/app/src/main/res/values-oc/strings.xml
@@ -53,7 +53,7 @@
Seleccionat
- %1$s es produch per @fork-maintainers.
+ %1$s es produch per Mozilla.
@@ -363,6 +363,12 @@
Colleccion de moduls complementaris modificada. Tampadura de l’aplicacion per aplicar las modificacions…
+
+
+ Lo modul complementari es pas pres en carga
+
+ Lo modul complementari es ja installat
+
Sincronizar ara
@@ -727,6 +733,8 @@
Telecargament suprimits%1$s suprimit
+
+ Cap de fichièr pas telecargat%1$d seleccionats
@@ -865,6 +873,8 @@
Emmagazinatge persistent
+
+ Contengut contrarotlat per DRMDemandar per autorizar
@@ -1091,6 +1101,8 @@
Libèra d’espaci d’emmagazinatgePermissions dels sites
+
+ TelecargamentsSuprimir las donadas de navegacion
diff --git a/app/src/main/res/values-pa-rIN/strings.xml b/app/src/main/res/values-pa-rIN/strings.xml
index d0c77e636..e65606abf 100644
--- a/app/src/main/res/values-pa-rIN/strings.xml
+++ b/app/src/main/res/values-pa-rIN/strings.xml
@@ -135,6 +135,8 @@
ਬੁੱਕਮਾਰਕ ਸੋਧੋਐਡ-ਆਨ
+
+ ਇਕਸਟੈਨਸ਼ਨਾਂਕੋਈ ਐਡ-ਆਨ ਨਹੀਂ ਹੈ
@@ -542,6 +544,10 @@
ਹੋਰ ਬੁੱਕਮਾਰਕਅਤੀਤ
+
+ ਨਵੀਂ ਟੈਬ
+
+ ਸਫ਼ੇ ‘ਚ ਲੱਭੋਸਿੰਕ ਕੀਤੀਆਂ ਟੈਬਾਂ
@@ -740,18 +746,22 @@
ਡਾਊਨਲੋਡ ਹਟਾਓਕੀ ਤੁਸੀਂ ਆਪਣੇ ਡਾਊਨਲੋਡਾਂ ਨੂੰ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?
-
- ਡਾਊਨਲੋਡ ਹਟਾਏ ਗਏ
+
+ ਡਾਊਨਲੋਡ ਹਟਾਏ ਗਏ
+
+ %1$s ਹਟਾਏ
- ਡਾਊਨਲੋਡ ਨਹੀਂ ਹੈ
+ ਕੋਈ ਡਾਊਨਲੋਡ ਕੀਤੀ ਫਾਇਲ ਨਹੀਂ ਹੈ%1$d ਚੁਣੇਖੋਲ੍ਹੋ
-
- ਹਟਾਓ
+
+
+
+ ਹਟਾਓ
@@ -875,6 +885,8 @@
ਸੂਚਨਾਵਾਂਪੱਕੀ ਸਟੋਰੇਜ਼
+
+ DRM-ਕੰਟਰੋਲ ਕੀਤੀ ਸਮੱਗਰੀਆਗਿਆ ਲਈ ਪੁੱਛੋ
@@ -1102,6 +1114,8 @@
ਸਟੋਰੇਜ਼ ਥਾਂ ਖਾਲੀ ਕਰਦਾ ਹੈਸਾਈਟ ਇਜਾਜ਼ਤਾਂ
+
+ ਡਾਊਨਲੋਡਬਰਾਊਜ਼ ਕਰਨ ਵਾਲਾ ਡਾਟਾ ਹਟਾਓ
@@ -1225,8 +1239,8 @@
ਤੁਹਾਡੀ ਪਰਦੇਦਾਰੀ
- ਅਸੀਂ %s ਨੂੰ ਤੁਹਾਨੂੰ ਕੰਟਰੋਲ ਦੇਣ ਵਾਸਤੇ ਬਣਾਇਆ ਹੈ ਕਿ ਤੁਸੀਂ ਆਨਲਾਈਨ ਕੀ ਸਾਂਝਾ ਕਰਦੇ ਹੋ ਅਤੇ ਸਾਡੇ ਨਾਲ ਤੁਸੀਂ ਕੀ ਸਾਂਝਾ ਕਰਦੇ ਹੋ।
-
+ ਅਸੀਂ %s ਨੂੰ ਇੰਝ ਬਣਾਇਆ ਹੈ ਕਿ ਤੁਹਾਡੇ ਕੋਲ ਪੂਰਾ ਕੰਟਰੋਲ ਹੋਵੇ ਕਿ ਤੁਸੀਂ
+ਆਨਲਾਈਨ ਕੀ ਸਾਂਝਾ ਕਰਦੇ ਹੋ ਅਤੇ ਸਾਡੇ ਨਾਲ ਕੀ ਸਾਂਝਾ ਕਰਦੇ ਹੋ।ਸਾਡੀ ਪਰਦੇਦਾਰੀ ਸੂਚਨਾ ਨੂੰ ਪੜ੍ਹੋ
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 8886d69c2..d6ffee801 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -131,6 +131,8 @@
Edytuj zakładkęDodatki
+
+ RozszerzeniaNie ma żadnych dodatków
@@ -532,6 +534,10 @@
Pozostałe zakładkiHistoria
+
+ Nowa karta
+
+ Znajdź na stronieKarty z innych urządzeń
@@ -1095,6 +1101,8 @@
Zwalnia miejsce na urządzeniuUprawnienia witryn
+
+ Pobrane plikiUsuń dane przeglądania
@@ -1218,9 +1226,7 @@
Twoja prywatność
- %s został zaprojektowany tak, aby dać Ci kontrolę nad tym,
- co udostępniasz w sieci i czym dzielisz się z nami.
-
+ %s został zaprojektowany tak, aby dać Ci kontrolę nad tym, co udostępniasz w sieci i czym dzielisz się z nami.Poznaj nasze zasady ochrony prywatności
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 5b7d1af11..eaad1028f 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -130,6 +130,8 @@
Editar favoritoExtensões
+
+ ExtensõesNenhuma extensão aqui
@@ -528,6 +530,10 @@
Outros favoritosHistórico
+
+ Nova aba
+
+ Procurar na páginaAbas sincronizadas
@@ -1089,6 +1095,8 @@
Libera espaço de armazenamentoPermissões de sites
+
+ DownloadsLimpar dados de navegação
@@ -1209,7 +1217,7 @@
Sua privacidade
- Projetamos o %s para lhe dar o controle sobre o que você compartilha online e o que compartilha conosco.
+ Projetamos o %s para te dar controle sobre o que você compartilha online e o que você compartilha conosco.Leia nosso aviso de privacidade
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 735fa8edc..909deec7d 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -131,6 +131,8 @@
Editar marcadorExtras
+
+ ExtensõesSem extras aqui
@@ -530,6 +532,10 @@
Outros marcadoresHistórico
+
+ Novo separador
+
+ Localizar na páginaSeparadores sincronizados
@@ -1091,6 +1097,8 @@
Liberta espaço de armazenamentoPermissões do site
+
+ TransferênciasEliminar dados de navegação
@@ -1213,8 +1221,7 @@
A sua privacidade
- Nós desenhamos o %s para lhe dar mais controlo sobre o que partilha
- na Internet e conosco.
+ Nós desenhámos o %s para lhe dar mais controlo sobre o que partilha online e o que partilha connosco.Leia a nossa política de privacidade
diff --git a/app/src/main/res/values-rm/strings.xml b/app/src/main/res/values-rm/strings.xml
index 775b171c6..7736f8f3d 100644
--- a/app/src/main/res/values-rm/strings.xml
+++ b/app/src/main/res/values-rm/strings.xml
@@ -128,6 +128,8 @@
Modifitgar il segnapaginaSupplements
+
+ ExtensiunsNagins supplements qua
@@ -523,6 +525,10 @@
Auters segnapaginasCronologia
+
+ Nov tab
+
+ Tschertgar en la paginaTabs sincronisads
@@ -1085,6 +1091,8 @@
Permissiuns per websites
+
+ TelechargiadasStizzar las datas da navigaziun
@@ -1206,8 +1214,7 @@
Tia sfera privata
- Nus avain creà %s per che ti possias controllar tge che ti cundividas
- cun l\'internet e tge che ti cundividas cun nus.
+ Nus avain creà %s per che ti possias controllar tge che ti cundividas en l\'internet e tge che ti cundividas cun nus.Legia nossas directivas per la protecziun da datas
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 8ef2a099f..758628cfa 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -140,6 +140,8 @@
Изменить закладкуДополнения
+
+ РасширенияДополнения отсутствуют
@@ -550,6 +552,10 @@
Другие закладкиИстория
+
+ Новая вкладка
+
+ Найти на страницеОблачные вкладки
@@ -1132,6 +1138,8 @@
Освободит местоРазрешения для сайтов
+
+ ЗагрузкиУдалить данные веб-сёрфинга
@@ -1267,9 +1275,7 @@
- Мы создали %s, чтобы предоставить вам контроль над тем,
- чем вы делитесь — как в Интернете, так и с нами.
-
+ Мы создали %s, чтобы предоставить вам контроль над тем, чем вы делитесь — как в Интернете, так и с нами.Уведомление о конфиденциальности
diff --git a/app/src/main/res/values-sat/strings.xml b/app/src/main/res/values-sat/strings.xml
index 0262c80be..7aef50afb 100644
--- a/app/src/main/res/values-sat/strings.xml
+++ b/app/src/main/res/values-sat/strings.xml
@@ -40,11 +40,11 @@
ᱵᱟᱪᱷᱟᱣᱮᱱᱟ %1$s
- ᱵᱟᱝ ᱪᱚᱭᱚᱱᱟᱜ%1$s
+ ᱵᱟᱝ ᱵᱟᱪᱷᱟᱣᱟᱜ%1$sᱵᱟᱹᱰ ᱟᱠᱟᱱᱟ ᱢᱟᱹᱴᱤᱥᱤᱞᱮᱼᱴ ᱢᱳᱰ
- ᱢᱟᱹᱞᱴᱤᱥᱤᱞᱮᱠᱼᱴ ᱢᱳᱰ ᱵᱚᱞᱚ ᱮᱱᱟᱢ, ᱛᱩᱢᱟᱹᱞ ᱥᱟᱸᱪᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱨᱮᱵ ᱠᱚ ᱪᱚᱭᱚᱱ ᱢᱮ
+ ᱢᱟᱹᱞᱴᱤᱥᱤᱞᱮᱠᱼᱴ ᱢᱳᱰ ᱵᱚᱞᱚ ᱮᱱᱟᱢ, ᱛᱩᱢᱟᱹᱞ ᱥᱟᱸᱪᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱨᱮᱵ ᱠᱚ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ᱵᱟᱪᱷᱟᱣᱮᱱᱟ
@@ -95,7 +95,7 @@
ᱵᱟᱹᱰ
- ᱠᱷᱩᱞᱟᱹ ᱴᱮᱵ ᱞᱮᱭᱟᱩᱴ ᱠᱚ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾ ᱥᱟᱡᱟᱣ ᱴᱷᱮᱱ ᱪᱟᱞᱟᱣ ᱠᱟᱛᱮᱫᱽ ᱴᱮᱵ ᱣᱤᱣ ᱞᱟᱛᱟᱨ ᱨᱮ ᱜᱨᱤᱰ ᱪᱚᱭᱚᱱ ᱢᱮ ᱾
+ ᱠᱷᱩᱞᱟᱹ ᱴᱮᱵ ᱞᱮᱭᱟᱩᱴ ᱠᱚ ᱵᱚᱫᱚᱞ ᱢᱮ ᱾ ᱥᱟᱡᱟᱣ ᱴᱷᱮᱱ ᱪᱟᱞᱟᱣ ᱠᱟᱛᱮᱫᱽ ᱴᱮᱵ ᱣᱤᱣ ᱞᱟᱛᱟᱨ ᱨᱮ ᱜᱨᱤᱰ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ᱾ ᱥᱟᱡᱟᱣ ᱛᱮ ᱪᱟᱞᱟᱜᱽ ᱢᱮ
@@ -361,7 +361,7 @@
ᱱᱤᱛᱚᱜ ᱥᱭᱸᱠ ᱢᱮ
- ᱚᱠᱟ ᱥᱭᱸᱠ ᱨᱮᱭᱟᱜ ᱛᱟᱦᱮᱸᱱᱟ ᱪᱚᱭᱚᱱ ᱢᱮ
+ ᱚᱠᱟ ᱥᱭᱸᱠ ᱨᱮᱭᱟᱜ ᱛᱟᱦᱮᱸᱱᱟ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ᱦᱤᱛᱟᱹᱞ
@@ -419,7 +419,7 @@
ᱡᱷᱚᱛᱚ ᱠᱚ ᱥᱟᱭᱤᱴ ᱞᱟᱹᱜᱤᱛ ᱛᱮ ᱚᱱ ᱢᱮ
- ᱪᱚᱭᱚᱱᱟᱠᱟᱱ ᱫᱟᱭᱤᱴ ᱠᱚᱨᱮ ᱟᱢ ᱤᱪᱷᱟᱹ ᱚᱱᱩᱥᱟᱨ ᱜᱷᱮᱨ ᱮᱥᱮᱫ ᱵᱚᱸᱫᱚ ᱫᱟᱲᱮᱭᱟᱜᱼᱟᱢ ᱾
+ ᱵᱟᱪᱷᱟᱣᱟᱠᱟᱱ ᱫᱟᱭᱤᱴ ᱠᱚᱨᱮ ᱟᱢ ᱤᱪᱷᱟᱹ ᱚᱱᱩᱥᱟᱨ ᱜᱷᱮᱨ ᱮᱥᱮᱫ ᱵᱚᱸᱫᱚ ᱫᱟᱲᱮᱭᱟᱜᱼᱟᱢ ᱾ᱰᱷᱮᱨ ᱥᱮᱬᱟᱭ ᱢᱮ
@@ -595,7 +595,7 @@
ᱛᱩᱢᱟᱹᱞ ᱨᱮ ᱥᱟᱸᱪᱟᱣ ᱢᱮ
- ᱪᱚᱭᱚᱱ
+ ᱵᱟᱪᱷᱟᱣᱡᱷᱚᱛᱚ ᱴᱮᱵ ᱠᱚ ᱦᱟᱹᱴᱧ ᱢᱮ
@@ -615,13 +615,13 @@
ᱵᱚᱸᱫᱽ
- ᱪᱚᱭᱚᱱ ᱟᱠᱟᱱ ᱴᱮᱵᱽ ᱠᱚ ᱦᱟᱹᱴᱤᱧ ᱢᱮ
+ ᱵᱟᱪᱷᱟᱣ ᱟᱠᱟᱱ ᱴᱮᱵᱽ ᱠᱚ ᱦᱟᱹᱴᱤᱧ ᱢᱮ
- ᱪᱚᱭᱚᱱ ᱟᱠᱟᱱ ᱴᱮᱵ ᱢᱮᱱᱭᱩ ᱠᱚ
+ ᱵᱟᱪᱷᱟᱣ ᱟᱠᱟᱱ ᱴᱮᱵ ᱢᱮᱱᱭᱩ ᱠᱚᱴᱮᱵ ᱛᱩᱢᱟᱹᱞ ᱠᱷᱚᱱ ᱚᱪᱚᱜ ᱢᱮ
- ᱴᱮᱵ ᱠᱚ ᱪᱚᱭᱚᱱ ᱢᱮ
+ ᱴᱮᱵ ᱠᱚ ᱵᱟᱪᱷᱟᱣ ᱢᱮᱴᱮᱵ ᱵᱚᱸᱫᱚᱭ ᱢᱮ
@@ -1203,11 +1203,6 @@
ᱟᱢᱟᱜ ᱱᱤᱥᱚᱱ
-
- ᱟᱞᱮ %s ᱞᱮ ᱰᱤᱡᱟᱭᱤᱱ ᱟᱠᱟᱫᱼᱟ ᱚᱠᱟ ᱫᱚ ᱡᱟᱦᱟᱱᱟᱜ ᱠᱚ ᱚᱱᱞᱟᱭᱤᱱ ᱦᱟᱹᱴᱤᱧ ᱨᱮ
- ᱟᱨ ᱚᱠᱟ ᱠᱚ ᱟᱞᱮ ᱦᱟᱹᱤᱧ ᱟᱞᱮᱭᱟᱢ, ᱜᱚᱲᱚ ᱮᱢᱟᱭ ᱾
- ᱟᱞᱮᱭᱟᱜ ᱱᱤᱥᱚᱱ ᱱᱚᱴᱤᱥ ᱯᱟᱲᱦᱟᱣ ᱯᱮ
@@ -1218,7 +1213,7 @@
- ᱩᱭᱦᱟᱹᱨ ᱪᱚᱭᱚᱱ ᱛᱟᱢ
+ ᱩᱭᱦᱟᱹᱨ ᱵᱟᱪᱷᱟᱣ ᱛᱟᱢᱛᱤᱱᱟᱹᱜ ᱜᱟᱱ ᱵᱮᱴᱨᱭ ᱵᱚᱪᱚᱛ ᱢᱮ ᱟᱨ ᱟᱢᱟᱜ ᱢᱮᱫ ᱵᱟᱸᱪᱟᱣ ᱛᱟᱢ ᱧᱩᱛ ᱩᱭᱦᱟᱹᱨ ᱪᱟᱹᱞᱩ ᱠᱟᱛᱮ ᱾
@@ -1287,7 +1282,7 @@
ᱠᱩᱥᱤᱭᱟᱜ
- ᱪᱚᱭᱚᱱ ᱢᱮ ᱚᱠᱟ ᱯᱟᱧᱡᱟ ᱫᱟᱱᱟᱲ ᱠᱚ ᱟᱨ ᱥᱠᱨᱤᱯᱴ ᱞᱚ ᱵᱞᱚᱠ ᱠᱚᱣᱟ ᱾
+ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ᱚᱠᱟ ᱯᱟᱧᱡᱟ ᱫᱟᱱᱟᱲ ᱠᱚ ᱟᱨ ᱥᱠᱨᱤᱯᱴ ᱞᱚ ᱵᱞᱚᱠ ᱠᱚᱣᱟ ᱾ᱠᱚᱥᱴᱚᱢ ᱜᱷᱮᱨ ᱮᱥᱮᱫ ᱯᱨᱚᱴᱮᱠᱥᱚᱱ ᱛᱮ ᱪᱮᱫ ᱵᱞᱚᱠ ᱟᱹᱠᱟᱱᱟ
@@ -1658,7 +1653,7 @@
ᱴᱚᱯ ᱥᱟᱭᱤᱴ ᱞᱤᱢᱤᱴ ᱥᱮᱴᱮᱨᱮᱱᱟ
- ᱱᱟᱶᱟ ᱴᱟᱹᱯ ᱥᱟᱭᱤᱴ ᱥᱮᱞᱮᱫᱽ ᱞᱟᱹᱜᱤᱫ, ᱢᱤᱫᱴᱟᱹᱝ ᱚᱰᱚᱠ ᱢᱮ ᱾ ᱥᱟᱭᱤᱴ ᱴᱤᱯᱟᱹᱣ ᱟᱨ ᱚᱛᱟ ᱛᱷᱤᱨ ᱠᱟᱛᱮ ᱪᱚᱭᱚᱱ ᱢᱮ ᱟᱨ ᱚᱰᱚᱠ ᱢᱮ ᱾
+ ᱱᱟᱶᱟ ᱴᱟᱹᱯ ᱥᱟᱭᱤᱴ ᱥᱮᱞᱮᱫᱽ ᱞᱟᱹᱜᱤᱫ, ᱢᱤᱫᱴᱟᱹᱝ ᱚᱰᱚᱠ ᱢᱮ ᱾ ᱥᱟᱭᱤᱴ ᱴᱤᱯᱟᱹᱣ ᱟᱨ ᱚᱛᱟ ᱛᱷᱤᱨ ᱠᱟᱛᱮ ᱵᱟᱪᱷᱟᱣ ᱢᱮ ᱟᱨ ᱚᱰᱚᱠ ᱢᱮ ᱾ᱴᱷᱤᱠ, ᱵᱟᱰᱟᱭ ᱠᱮᱜᱼᱟᱹᱧ
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index cfba99b83..08904d2f5 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -131,6 +131,8 @@
Upraviť záložkuDoplnky
+
+ RozšíreniaNemáte žiadne doplnky
@@ -535,6 +537,10 @@
Ostatné záložkyHistória
+
+ Nová karta
+
+ Hľadať na stránkeSynchronizované karty
@@ -1097,6 +1103,8 @@
Uvolní priestor v zariadeníPovolenia stránok
+
+ Prevzaté súboryOdstrániť údaje o prehliadaní
@@ -1219,9 +1227,7 @@
Vaše súkromie
- %s vám dáva kontrolu nad tým, čo zdieľate
- na internete a čo zdieľate s nami.
-
+ %s vám dáva kontrolu nad tým, čo zdieľate na internete a čo zdieľate s nami.Prečítajte si naše zásady ochrany súkromia
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index a03e3a9c5..b722f3d88 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -131,6 +131,8 @@
Uredi zaznamekDodatki
+
+ RazširitveTukaj ni dodatkov
@@ -536,6 +538,10 @@
Drugi zaznamkiZgodovina
+
+ Nov zavihek
+
+ Najdi na straniSinhronizirani zavihki
@@ -735,6 +741,8 @@
Ste prepričani, da želite počistiti vaše prenose?Prenosi odstranjeni
+
+ %1$s odstranjenNi prenesenih datotekDovoljenja strani
+
+ PrenosiIzbriši podatke brskanja
@@ -1220,8 +1230,7 @@
- %s smo zasnovali tako, da vam omogočimo nadzor nad tem, kaj delite
- na spletu in kaj delite z nami.
+ %s smo zasnovali tako, da vam omogočimo nadzor nad tem, kaj delite na spletu in kaj delite z nami.Preberite naše obvestilo o zasebnosti
diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml
index 1a109fabd..b5c2d8f05 100644
--- a/app/src/main/res/values-sq/strings.xml
+++ b/app/src/main/res/values-sq/strings.xml
@@ -1084,6 +1084,8 @@
Liron hapësirë depozitimiLeje sajti
+
+ ShkarkimeFshi të dhëna shfletimi
@@ -1207,9 +1209,7 @@
Privatësia juaj
- E kemi hartuar %s për t’ju dhënë kontroll mbi ç’ndani me
- të tjerë në internet dhe ç’ndani me ne.
-
+ E kemi hartuar %s për t’ju dhënë kontroll mbi ç’ndani me të tjerë në internet dhe ç’ndani me ne.Lexoni shënimin tonë mbi privatësinë
diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml
index 6cedf5a24..bec6b2f74 100644
--- a/app/src/main/res/values-sr/strings.xml
+++ b/app/src/main/res/values-sr/strings.xml
@@ -128,6 +128,8 @@
Уреди забелешкуДодаци
+
+ ПроширењаОвде нема додатака
@@ -529,6 +531,10 @@
Остале забелешкеИсторијат
+
+ Нови језичак
+
+ Пронађи на странициСинхронизовани језичци
@@ -1088,6 +1094,8 @@
Ослобађа простор у складиштуДозволе на страници
+
+ ПреузимањаОбриши податке прегледања
@@ -1212,8 +1220,7 @@
Ваша приватност
- Дизајнирали смо %s тако да имате пуну контролу над оним што делите
- на мрежи и што делите са нама.
+ Дизајнирали смо %s тако да имате пуну контролу над оним што делите на мрежи и са нама.Прочитајте наше обавештење о приватности
diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml
index d21b90849..7ce8f19e5 100644
--- a/app/src/main/res/values-su/strings.xml
+++ b/app/src/main/res/values-su/strings.xml
@@ -1097,6 +1097,8 @@
Bébaskeun rohang panyimpenanIdin loka
+
+ UndeuranPupus data nyungsi
@@ -1221,8 +1223,7 @@
Salindungan anjeun
- Kami geus ngarancang %s sangkan anjeun bisa ngatur naon anu dibagikeun
- jering jeung naon anu dibagikeun ka kami.
+ Kami geus ngarancang %s sangkan anjeun bisa ngatur naon anu dibagikeun onlén jeung naon anu dibagikeun ka kami.Maca wawaran salindungan kami
diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml
index e14d8e37c..089dd5782 100644
--- a/app/src/main/res/values-sv-rSE/strings.xml
+++ b/app/src/main/res/values-sv-rSE/strings.xml
@@ -30,7 +30,7 @@
%1$s öppna flikar. Tryck för att växla mellan flikar.
- %1$d markerad
+ %1$d markeradeLägg till ny samling
@@ -132,6 +132,8 @@
Redigera bokmärkeTillägg
+
+ TilläggInga tillägg här
@@ -537,6 +539,10 @@
Andra bokmärkenHistorik
+
+ Ny flik
+
+ Hitta på sidanSynkade flikar
@@ -743,7 +749,7 @@
Inga nedladdade filer
- %1$d markerad
+ %1$d markeradeÖppen
@@ -1102,6 +1108,8 @@
Frigör lagringsutrymmeWebbplatsbehörigheter
+
+ HämtningarTa bort surfdata
@@ -1228,9 +1236,7 @@
Din integritet
- Vi har utformat %s för att ge dig kontroll över vad du delar
- online och vad du delar med oss.
-
+ Vi har utformat %s för att ge dig kontroll över vad du delar online och vad du delar med oss.Läs vår sekretesspolicy
diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml
index e4d441dc3..09fc2c4f1 100644
--- a/app/src/main/res/values-te/strings.xml
+++ b/app/src/main/res/values-te/strings.xml
@@ -507,6 +507,8 @@
చరిత్ర
+
+ కొత్త ట్యాబుసింకైన ట్యాబులు
@@ -537,6 +539,8 @@
ట్యాబులు
+
+ ట్యాబు వీక్షణజాబితా
@@ -1072,6 +1076,8 @@
సైటు అనుమతులు
+
+ దింపుకోళ్ళువిహరణ డేటా తొలగించు
@@ -1188,10 +1194,6 @@
అమరికలను తెరువుమీ అంతరంగికత
-
- మీరు ఆన్లైనులోనూ, మాతోనూ ఏమేం పంచుకుంటారన్నదానిపై
- మీకు నియంత్రణ ఇవ్వడానికి మేము %sని రూపొందించాము.మా గోప్యతా నోటీసును చదవండి
@@ -1350,6 +1352,9 @@
దారిమళ్ళింపు ట్రాకర్లు
+
+ తెలిసిన ట్రాకింగు వెబ్సైట్లు దారిమార్పుల ద్వారా అమర్చే కుకీలను తుడిచివేస్తుంది.
+
తోడ్పాటు
diff --git a/app/src/main/res/values-tg/strings.xml b/app/src/main/res/values-tg/strings.xml
index c7722a4d1..f380493ee 100644
--- a/app/src/main/res/values-tg/strings.xml
+++ b/app/src/main/res/values-tg/strings.xml
@@ -1089,6 +1089,8 @@
Фазои захирагоҳро озод намоедИҷозатҳои сомона
+
+ БоргириҳоНест кардани маълумоти тамошокунӣ
@@ -1214,9 +1216,7 @@
Махфияти шумо
- Мо %s-ро ҳамин тавр тарҳрезӣ кардаем, ки шумо тавонед он чизҳоеро, ки дар онлайн
- ва бо мо мубодила мекунед, идора намоед.
-
+ Мо %s-ро ҳамин тавр тарҳрезӣ кардаем, ки шумо тавонед он чизҳоеро, ки дар онлайн ва бо мо мубодила мекунед, идора намоед.Огоҳиномаи махфияти моро хонед
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 013f5573c..434e4e567 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -1085,6 +1085,8 @@
เพิ่มเนื้อที่เก็บข้อมูลสิทธิอนุญาตไซต์
+
+ ดาวน์โหลดลบข้อมูลการเรียกดู
@@ -1206,8 +1208,7 @@
ความเป็นส่วนตัวของคุณ
- เราได้ออกแบบ %s เพื่อให้คุณสามารถควบคุมสิ่งที่คุณต้องการแบ่งปัน
- ออนไลน์และสิ่งที่คุณต้องการแบ่งปันกับเราได้
+ เราได้ออกแบบ %s เพื่อให้คุณสามารถควบคุมสิ่งที่คุณต้องการแบ่งปันออนไลน์และสิ่งที่คุณต้องการแบ่งปันกับเราได้อ่านประกาศความเป็นส่วนตัวของเรา
diff --git a/app/src/main/res/values-tl/strings.xml b/app/src/main/res/values-tl/strings.xml
index d59696360..93f5ed5e4 100644
--- a/app/src/main/res/values-tl/strings.xml
+++ b/app/src/main/res/values-tl/strings.xml
@@ -9,10 +9,22 @@
Iba pang pagpipilianPaganahin ang private browsing
+
+ I-disable ang private browsing
+
+ Maghanap o ilagay ang address
+
+ Ipapakita rito ang iyong nakabukas na mga tab.
+
+ Ipapakita rito ang iyong mga pribadong tab.BaiduJD
+
+ 1 nakabukas na tab. I-tap para lumipat ng tab.
+
+ %1$s nakabukas na tab. I-tap para lumipat ng tab.%1$d ang napili
@@ -22,10 +34,18 @@
Pumili ng koleksyon
+
+ Umalis sa multiselect mode
+
+ I-save ang mga napiling tab sa collectionNapiling %1$sHindi napiling %1$s
+
+ Umalis na sa multiselect mode
+
+ Pumasok na sa multiselect mode, pumili ng mga tab para mai-save sa isang collectionNapili
@@ -43,11 +63,16 @@
Salamat na lang
+
+
+ Maaari mong itakda sa Firefox na kusang buksan ang mga link sa mga app.Magpunta sa SettingsAlisin
+
+ Kailangan ng camera access. Pumunta sa Android settings, i-tap ang permiso, at i-tap ang payagan.Magpunta sa settings
@@ -88,6 +113,8 @@
i-Edit ang BookmarkMga Add-on
+
+ Walang mga add-on ditoTulong
@@ -128,30 +155,50 @@
Powered by %1$sReader view
+
+ Isara ang reader viewBuksan sa appItsura
+
+ Hindi makakonekta. Hindi kilalang URL scheme.
+
Mamili ng wikaHanapin
+
+ Sundin ang wika sa device
+
i-ScanSearch engine
+
+ Mga search engine setting
+
+ Ngayon naman, maghanap gamit ang:
+
+ Punan ang link mula sa clipboardPayaganHuwag Payagan
+
+ Payagan ang mga mungkahi sa paghahanap sa mga pribadong session?Alamin
+
+
+ Magbukas ng bagong Firefox tabMaghanap
@@ -175,6 +222,10 @@
Address barTulong
+
+ I-rate sa Google Play
+
+ Magbigay ng punaTungkol sa %1$s
@@ -182,10 +233,24 @@
Your RightsMga Password
+
+ Mga credit card at tirahan
+
+ Itakda bilang default browserPribasiyaPribasiya at Seguridad
+
+ Mga site permission
+
+ Private browsing
+
+ Buksan ang mga link sa pribadong tab
+
+ Payagan ang mga screenshot sa private browsing
+
+ Magdagdag ng private browsing shortcutAccessibility
@@ -202,27 +267,77 @@
GesturesCustomize
+
+ Mag-sync ng mga bookmark, kasaysayan, at iba pa gamit ang iyong Firefox AccountFirefox Account
+
+ Muling kumonekta para maipagpatuloy ang pag-syncWika
+
+ Mga pamimilian sa data
+
+ Pagkolekta ng dataDeveloper tools
+
+ Remote debugging via USB
+
+ Ipakita ang mga search engine
+
+ Ipakita ang mga mungkahi sa paghanap
+
+ Ipakita ang voice search
+
+ Ipakita sa mga pribadong session
+
+ Ipakita ang mga mungkahi mula sa clipboard
+
+ Ipakita ang kasaysayan ng pag-browse
+
+ Hanapin sa mga bookmark
+
+ Hanapin sa mga naka-sync na tab
+
+ Mga account setting
+
+ Autocomplete URLs
+
+ Buksan ang mga link sa mga app
+
+ Hiwalay na download managerAdd-ons
+
+ Mga abiso
+
OKKanselahin
+
+ Pangalan ng collection
+
+ May-ari ng collection (User ID)
+
+
+
+ Hindi suportado ang add-on
+
+ Naka-install na ang add-on
+
Sync now
+
+ Piliin alin ang isi-syncKasaysayan
@@ -236,15 +351,55 @@
Ngalan ng device
+
+ Hindi maaaring blangko ang pangalan ng device.Syncing…
+
+ Bigong pag-sync. Huling matagumpay na pag-sync: %s
+
+ Bigong pag-sync. Huling pag-sync: hindi pa ni minsan
+
+ Huling na-sync: %s
+
+ Huling na-sync: hindi pa ni minsan%1$s ng %2$s %3$s
+
+
+ Mga natanggap na tab
+
+ May natanggap na tab
+
+ May natanggap na mga tab
+
+ Tab mula sa %s
+
+
+
+ Tracking Protection
+
+ Tracking Protection
+
+ Harangin ang content at mga script na sumusubaybay sa iyo online
+
+ Mga exception
+
+ Nakasara ang Tracking Protection sa mga website na ito
+
+ Buksan sa lahat ng mga site
+
+ Nakakapag-disable ka ng tracking protection sa mga piling site gamit ang mga exception.
+
+ Alamin
+
Telemetry
+
+ Usage at technical dataMarketing data
@@ -292,12 +447,42 @@
Madilim
+
+ Sundin ang tema ng device
+
+
+
+ Hilahin pababa para mag-refresh
+
+ Mag-scroll para maitago ang toolbar
+
+ Mag-swipe patagilid sa toolbar para makalipat ng tab
+
+ Mag-swipe pataas sa toolbar para maipakita ang mga tab
+
+
+
+ Mga sessionMga ScreenshotMga DownloadMga Bookmark
+
+ Mga Desktop Bookmark
+
+ Menu ng mga Bookmark
+
+ Bookmarks Toolbar
+
+ Iba pang mga Bookmark
+
+ Kasaysayan
+
+ Mga naka-sync na tab
+
+ Reading ListHanapin
@@ -305,6 +490,10 @@
Isara
+
+ Mga isinarang tab kamakailan
+
+ Ipakita ang kumpletong kasaysayan%d mga tab
@@ -312,6 +501,9 @@
%d is a placeholder for the number of tabs selected. -->
%d tab
+
+ Walang mga isinarang tab kamakailan
+
Mga Tab
@@ -323,25 +515,75 @@
Isara ang mga tab
+
+ Manu-mano
+
+ Pagkatapos ng isang araw
+
+ Pagkatapos ng isang linggo
+
+ Pagkatapos ng isang buwan
+
+
+ Manu-manong isara
+
+ Isara pagkatapos ng isang araw
+
+ Isara pagkatapos ng isang linggo
+
+ Isara pagkatapos ng isang buwan
+
Buksan ang mga tab
+
+ Pribadong session
+
+ Mga pribadong tabMagdagdag ng Tab
+
+ Idagdag sa pribadong tabPribadoBuksan ang mga Tab
+
+ I-save sa collectionPumili
+
+ Ibahagi lahat ng mga tab
+
+ Mga isinarang tab kamakailan
+
+ Mga tab setting
+
+ Isara lahat ng mga tabBagong TabBookmarkIsara
+
+ Ibahagi ang mga piniling mga tab
+
+ Piliin ang mga tab
+
+ Isara ang tab
+
+ Isara ang tab na %s
+
+ Buksan ang menu ng mga tab
+
+ Isara lahat ng mga tabIbahagi ang mga tab
+
+ I-save ang mga tab sa collection
+
+ Menu ng tabIbahagi ang tab
@@ -352,31 +594,131 @@
i-SaveIbahagi
-
+
+ Kasalukuyang session image
+
+ I-save sa collection
+
+ Burahin ang collection
+
+ Baguhin ang pangalan ng collection
+
+ Buksan ang mga tab
+
+ Pangalan ng collection
+
+ Palitan ng pangalan
+
Alisin
+
+ Burahin sa kasaysayan
+
+ %1$s (Pribadong Mode)i-Save
+
+
+ Burahin ang kasaysayan
+
+ Sigurado ka bang gusto mong burahin ang iyong kasaysayan?
+
+ Nabura na ang Kasaysayan
+
+ Binura na ang %1$s
+
+ AlisinKopyahinIbahagi
+
+ Buksan sa bagong tab
+
+ Buksan sa pribadong tab
+
+ Burahin
+
+ Napili na ang %1$d
+
+ Burahin ang %1$d itemNgayonKahapon
+
+ Nakaraang 24 oras
+
+ Huling 7 araw
+
+ Huling 30 arawMas luma
+
+ Walang kasaysayan dito
+
+
+
+ Burahin ang mga download
+
+ Sigurado ka bang gusto mong burahin lahat ng iyong mga download?
+
+ Natanggal na ang mga Download
+
+ Natanggal na ang %1$s
+
+ Walang mga naka-download na file
+
+ Napili na ang %1$dBuksan
-
- Burahin
+
+ Tanggalin
+
+
+
+
+ Pasensya na. Hindi kayang ma-load ng %1$s ang pahina.
+
+ Maaari mong subukang i-restore o isara ang tab na ito sa baba.
+
+ Ipadala ang crash report sa Mozilla
+
+ Isara ang tab
+
+ I-restore ang tab
+
+
+ Mga pagpipilian sa session
+
+
+ Ibahagi ang session
+
+
+ Pumili ng folder
+
+ Sigurado ka bang gusto mong burahin ang folder na ito?
+
+ Buburahin ng %s ang mga napiling item.
+
+ Nabura na ang %1$s
+
+ Magdagdag ng folder
+
+ Nalikha na ang bookmark.
+
+ Nai-save na ang bookmark!
+
+ I-EDITi-Edit
@@ -386,10 +728,23 @@
Ibahagi
+
+ Buksan sa bagong tab
+
+ Buksan sa pribadong tabBurahini-Save
+
+ Napili na ang %1$d
+
+ i-Edit ang bookmark
+
+ i-Edit ang folder
+
+ Mag-sign in para makita ang mga naka-sync na bookmarkURL
@@ -397,6 +752,31 @@
PANGALAN
+
+ Magdagdag ng folder
+
+ Pumili ng folder
+
+ Kailangang may pamagat
+
+ Di-wastong URL
+
+ Walang mga bookmark dito
+
+ Binura na ang %1$s
+
+ Nabura na ang mga bookmark
+
+ Binubura ang mga napiling folder
+
+ I-UNDO
+
+
+
+ Mga permiso
+
+ Pumunta sa mga SettingCamera
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index e830679b7..f6016615c 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -130,6 +130,8 @@
Yer imini düzenleEklentiler
+
+ EklentilerHiç eklenti yok
@@ -529,6 +531,10 @@
Diğer yer imleriGeçmiş
+
+ Yeni sekme
+
+ Sayfada bulEşitlenmiş sekmeler
@@ -1088,6 +1094,8 @@
Depolama alanını boşaltırSite izinleri
+
+ İndirilenlerGezinti verilerini sil
@@ -1210,7 +1218,7 @@
Gizliliğiniz
- %s, internette başkalarıyla ve bizimle neleri paylaşacağınızın kontrolünü size verir.
+ %s, internette başkalarıyla ve bizimle neleri paylaşacağınızın kontrolünü size verir.Gizlilik bildirimimizi okuyun
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index eab79de45..aca454f05 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -130,6 +130,8 @@
Змінити закладкуДодатки
+
+ РозширенняНемає додатків
@@ -535,6 +537,10 @@
Інші закладкиІсторія
+
+ Нова вкладка
+
+ Знайти на сторінціСинхронізовані вкладки
@@ -565,11 +571,11 @@
Вкладки
- Подання вкладки
+ Подання вкладок
- Перелік
+ Списком
- Сітка
+ СіткоюЗакрити вкладки
@@ -1097,6 +1103,8 @@
Звільняє місце для зберіганняДозволи сайтів
+
+ ЗавантаженняВидалити дані перегляду
@@ -1222,8 +1230,7 @@
- Ми створили %s, щоб надати вам контроль над тим, чим ви ділитесь
- в Інтернеті та з нами.
+ Ми створили %s, щоб надати вам контроль над тим, чим ви ділитесь в Інтернеті та з нами.Повідомлення про приватність
@@ -1530,7 +1537,7 @@
Увімкніть, щоб дозволити масштабування, навіть на вебсайтах, які це не дозволяють.
- Назва (A-Z)
+ Назва (А-Я)Востаннє використано
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 9dee39d04..c7ac27a9a 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -131,6 +131,8 @@
Chỉnh sửa dấu trangTiện ích
+
+ Tiện ích mở rộngKhông có tiện ích nào ở đây
@@ -526,6 +528,10 @@
Dấu trang khácLịch sử
+
+ Thẻ mới
+
+ Tìm trong trangCác thẻ đã đồng bộ
@@ -1083,6 +1089,8 @@
Giải phóng không gian lưu trữQuyền hạn trang web
+
+ Tải xuốngXóa dữ liệu duyệt web
@@ -1203,9 +1211,7 @@
Quyền riêng tư của bạn
- Chúng tôi đã thiết kế %s để cung cấp cho bạn quyền kiểm soát những gì bạn chia sẻ
- trực tuyến và những gì bạn chia sẻ với chúng tôi.
-
+ Chúng tôi đã thiết kế %s để cung cấp cho bạn quyền kiểm soát những gì bạn chia sẻ trực tuyến và những gì bạn chia sẻ với chúng tôi.Đọc thông báo bảo mật của chúng tôi
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 0fc25b395..d7137ead5 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -58,7 +58,7 @@
已选择
- %1$s 由 @fork-maintainers 倾力打造。
+ %1$s 由 Mozilla 倾力打造。
@@ -134,6 +134,8 @@
编辑书签附加组件
+
+ 扩展这里没有附加组件
@@ -374,7 +376,7 @@
- 附加组件不支持
+ 不支持的附加组件附加组件已安装
@@ -531,7 +533,7 @@
屏幕截图
- 下载项
+ 下载书签
@@ -544,6 +546,10 @@
其他书签历史
+
+ 新建标签页
+
+ 在页面中查找受同步的标签页
@@ -738,11 +744,11 @@
- 删除下载项
+ 清除下载记录
- 您确定要清除下载项吗?
+ 您确定要清除下载记录吗?
- 下载项已移除
+ 下载记录已清除已移除 %1$s
@@ -1128,6 +1134,8 @@
释放存储空间站点权限
+
+ 下载项删除浏览数据
@@ -1251,7 +1259,7 @@
您的隐私权
- %s 的设计目标是让您可以控制要在网上披露哪些内容,以及告诉我们哪些信息。
+ %s 的设计旨在让您可以控制要在网上披露哪些内容,以及告诉我们哪些信息。阅读我们的隐私声明
@@ -1411,7 +1419,7 @@
用户支持
- 崩溃
+ 崩溃信息隐私声明
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 6d5969c4a..fc1d25a6f 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -131,6 +131,8 @@
編輯書籤附加元件
+
+ 擴充套件無附加元件
@@ -536,6 +538,10 @@
其他書籤瀏覽紀錄
+
+ 開新分頁
+
+ 在頁面中搜尋同步的分頁
@@ -1120,6 +1126,8 @@
可清理出儲存空間網站權限
+
+ 下載項目刪除瀏覽資料
@@ -1240,7 +1248,7 @@
您的隱私權
- 我們將 %s 設計成讓您可以完整控制要在網路上分享哪些東西、以及與我們分享哪些東西。
+ 我們將 %s 設計成讓您可以完整控制要在網路上分享哪些東西、以及與我們分享哪些東西。閱讀我們的隱私權公告
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 27c046c7d..1143fed8a 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -30,4 +30,13 @@
@string/all@string/private_string
+
+ "resource://"
+ "chrome://
+ "about:"
+
+ @string/resource_scheme
+ @string/chrome_scheme
+ @string/about_scheme
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 845bb44c4..169b32455 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -257,6 +257,7 @@
@color/accent_bright_light_theme@color/mozac_widget_favicon_background_light_theme@color/mozac_widget_favicon_border_light_theme
+ @color/accent_bright_light_theme@color/tab_tray_item_text_light_theme
@@ -372,6 +373,7 @@
@color/accent_high_contrast_dark_theme
+ #1F000000#20123A#FFFFFF#F9F9FB
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 0fee3ee07..27617106a 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -37,6 +37,8 @@
56dp16dp12dp
+ 48dp
+ 48dp16dp68dp
@@ -59,7 +61,7 @@
48dp
- 28dp
+ 48dp48dp16dp18dp
diff --git a/app/src/main/res/values/preference_keys.xml b/app/src/main/res/values/preference_keys.xml
index b0a490252..b5d9797de 100644
--- a/app/src/main/res/values/preference_keys.xml
+++ b/app/src/main/res/values/preference_keys.xml
@@ -27,11 +27,13 @@
pref_key_delete_cookies_on_quitpref_key_delete_caches_on_quitpref_key_delete_permissions_on_quit
+ pref_key_delete_downloads_on_quitpref_key_delete_open_tabs_nowpref_key_delete_browsing_history_nowpref_key_delete_cookies_nowpref_key_delete_caches_nowpref_key_delete_permissions_now
+ pref_key_delete_downloads_nowpref_key_delete_browsing_data_on_quit_categoriespref_key_last_known_mode_privatepref_key_addons
@@ -108,6 +110,7 @@
pref_key_show_site_exceptionspref_key_recommended_settingspref_key_custom_settings
+ pref_key_browser_feature_autoplaypref_key_browser_feature_autoplaypref_key_browser_feature_autoplay_inaudiblepref_key_browser_feature_persistent_storage
@@ -159,6 +162,7 @@
pref_key_tracking_protection_custom_tracking_content_selectpref_key_tracking_protection_custom_cryptominerspref_key_tracking_protection_custom_fingerprinters
+ pref_key_tracking_protection_redirect_trackerspref_key_tracking_protection_onboarding
diff --git a/app/src/main/res/values/static_strings.xml b/app/src/main/res/values/static_strings.xml
index 713c441ab..170e9b38a 100644
--- a/app/src/main/res/values/static_strings.xml
+++ b/app/src/main/res/values/static_strings.xml
@@ -34,10 +34,6 @@
Secret SettingsSecret Debug Info
-
- Show Top Frequently Visited Sites
-
- Wait Until First Paint To Show Page ContentShow Synced Tabs in the tabs tray
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f23b9163c..acde8e7e8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1076,6 +1076,8 @@
Frees up storage spaceSite permissions
+
+ DownloadsDelete browsing data
@@ -1210,9 +1212,7 @@
Your privacy
- We’ve designed %s to give you control over what you share
- online and what you share with us.
-
+ We’ve designed %s to give you control over what you share online and what you share with us.Read our privacy notice
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7b41618e9..c0d2b4ad2 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -107,6 +107,8 @@
@drawable/home_bottom_bar_background_top@android:color/transparent@color/primary_text_normal_theme
+
+ ?primaryText
diff --git a/app/src/main/res/xml/delete_browsing_data_quit_preferences.xml b/app/src/main/res/xml/delete_browsing_data_quit_preferences.xml
index 698061010..e125809d3 100644
--- a/app/src/main/res/xml/delete_browsing_data_quit_preferences.xml
+++ b/app/src/main/res/xml/delete_browsing_data_quit_preferences.xml
@@ -34,8 +34,13 @@
android:summary="@string/preferences_delete_browsing_data_cached_files_subtitle"
android:title="@string/preferences_delete_browsing_data_cached_files" />
+
+
+ android:title="@string/preferences_delete_browsing_data_downloads" />
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
index c8476fdba..84ee88031 100644
--- a/app/src/main/res/xml/preferences.xml
+++ b/app/src/main/res/xml/preferences.xml
@@ -11,7 +11,8 @@
android:key="@string/pref_key_sign_in"
android:layout="@layout/sign_in_preference"
android:summary="@string/preferences_sign_in_description"
- android:title="@string/preferences_sync" />
+ android:title="@string/preferences_sync"
+ app:allowDividerBelow="false"/>
+ android:title="@string/preferences_external_download_manager"/>
+
+
diff --git a/app/src/main/res/xml/tracking_protection_preferences.xml b/app/src/main/res/xml/tracking_protection_preferences.xml
index 226f213d1..f8742e610 100644
--- a/app/src/main/res/xml/tracking_protection_preferences.xml
+++ b/app/src/main/res/xml/tracking_protection_preferences.xml
@@ -70,6 +70,12 @@
android:key="@string/pref_key_tracking_protection_custom_fingerprinters"
android:layout="@layout/checkbox_left_preference_etp"
android:title="@string/preference_enhanced_tracking_protection_custom_fingerprinters" />
+ Unit
@MockK(relaxUnitFun = true) private lateinit var metrics: MetricController
@MockK(relaxUnitFun = true) private lateinit var tabCollectionStorage: TabCollectionStorage
- @MockK private lateinit var sessionManager: SessionManager
+ private lateinit var browserStore: BrowserStore
@Before
fun before() {
@@ -47,9 +53,15 @@ class DefaultCollectionCreationControllerTest {
)
every { store.state } answers { state }
+ browserStore = BrowserStore()
+
controller = DefaultCollectionCreationController(
- store, dismiss, metrics,
- tabCollectionStorage, sessionManager, testCoroutineScope
+ store,
+ browserStore,
+ dismiss,
+ metrics,
+ tabCollectionStorage,
+ testCoroutineScope
)
}
@@ -60,14 +72,13 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `GIVEN tab list WHEN saveCollectionName is called THEN collection should be created`() {
- val session = mockSession(sessionId = "session-1")
- val sessions = listOf(
- session,
- mockSession(sessionId = "session-2")
- )
- every { sessionManager.findSessionById("session-1") } returns session
- every { sessionManager.findSessionById("null-session") } returns null
- every { sessionManager.sessions } returns sessions
+ val tab1 = createTab("https://www.mozilla.org", id = "session-1")
+ val tab2 = createTab("https://www.mozilla.org", id = "session-2")
+
+ browserStore.dispatch(
+ TabListAction.AddMultipleTabsAction(listOf(tab1, tab2))
+ ).joinBlocking()
+
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE),
Tab("null-session", "", "", "", mediaState = MediaState.State.NONE)
@@ -76,7 +87,7 @@ class DefaultCollectionCreationControllerTest {
controller.saveCollectionName(tabs, "name")
verify { dismiss() }
- coVerify { tabCollectionStorage.createCollection("name", listOf(session)) }
+ coVerify { tabCollectionStorage.createCollection("name", listOf(tab1)) }
verify { metrics.track(Event.CollectionSaved(2, 1)) }
}
@@ -154,13 +165,13 @@ class DefaultCollectionCreationControllerTest {
@Test
fun `WHEN selectCollection is called THEN add tabs should be added to collection`() {
- val session = mockSession(sessionId = "session-1")
- val sessions = listOf(
- session,
- mockSession(sessionId = "session-2")
- )
- every { sessionManager.findSessionById("session-1") } returns session
- every { sessionManager.sessions } returns sessions
+ val tab1 = createTab("https://www.mozilla.org", id = "session-1")
+ val tab2 = createTab("https://www.mozilla.org", id = "session-2")
+
+ browserStore.dispatch(
+ TabListAction.AddMultipleTabsAction(listOf(tab1, tab2))
+ ).joinBlocking()
+
val tabs = listOf(
Tab("session-1", "", "", "", mediaState = MediaState.State.NONE)
)
@@ -169,7 +180,7 @@ class DefaultCollectionCreationControllerTest {
controller.selectCollection(collection, tabs)
verify { dismiss() }
- coVerify { tabCollectionStorage.addTabsToCollection(collection, listOf(session)) }
+ coVerify { tabCollectionStorage.addTabsToCollection(collection, listOf(tab1)) }
verify { metrics.track(Event.CollectionTabsAdded(2, 1)) }
}
@@ -246,14 +257,4 @@ class DefaultCollectionCreationControllerTest {
verify { store.dispatch(CollectionCreationAction.StepChanged(SaveCollectionStep.SelectCollection, 2)) }
}
-
- private fun mockSession(
- sessionId: String? = null,
- isPrivate: Boolean = false,
- isCustom: Boolean = false
- ) = mockk {
- sessionId?.let { every { id } returns it }
- every { private } returns isPrivate
- every { isCustomTabSession() } returns isCustom
- }
}
diff --git a/app/src/test/java/org/mozilla/fenix/components/PermissionStorageTest.kt b/app/src/test/java/org/mozilla/fenix/components/PermissionStorageTest.kt
new file mode 100644
index 000000000..631396647
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/components/PermissionStorageTest.kt
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.components
+
+import androidx.paging.DataSource
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runBlockingTest
+import mozilla.components.feature.sitepermissions.SitePermissions
+import mozilla.components.feature.sitepermissions.SitePermissionsStorage
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(FenixRobolectricTestRunner::class)
+class PermissionStorageTest {
+
+ @Test
+ fun `add permission`() = runBlockingTest {
+ val sitePermissions: SitePermissions = mockk(relaxed = true)
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ storage.add(sitePermissions)
+
+ verify { sitePermissionsStorage.save(sitePermissions) }
+ }
+
+ @Test
+ fun `find sitePermissions by origin`() = runBlockingTest {
+ val sitePermissions: SitePermissions = mockk(relaxed = true)
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ every { sitePermissionsStorage.findSitePermissionsBy(any()) } returns sitePermissions
+
+ val result = storage.findSitePermissionsBy("origin")
+
+ verify { sitePermissionsStorage.findSitePermissionsBy("origin") }
+
+ assertEquals(sitePermissions, result)
+ }
+
+ @Test
+ fun `update SitePermissions`() = runBlockingTest {
+ val sitePermissions: SitePermissions = mockk(relaxed = true)
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ storage.updateSitePermissions(sitePermissions)
+
+ verify { sitePermissionsStorage.update(sitePermissions) }
+ }
+
+ @Test
+ fun `get sitePermissions paged`() = runBlockingTest {
+ val dataSource: DataSource.Factory = mockk(relaxed = true)
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ every { sitePermissionsStorage.getSitePermissionsPaged() } returns dataSource
+
+ val result = storage.getSitePermissionsPaged()
+
+ verify { sitePermissionsStorage.getSitePermissionsPaged() }
+
+ assertEquals(dataSource, result)
+ }
+
+ @Test
+ fun `delete sitePermissions`() = runBlockingTest {
+ val sitePermissions: SitePermissions = mockk(relaxed = true)
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ storage.deleteSitePermissions(sitePermissions)
+
+ verify { sitePermissionsStorage.remove(sitePermissions) }
+ }
+
+ @Test
+ fun `delete all sitePermissions`() = runBlockingTest {
+ val sitePermissionsStorage: SitePermissionsStorage = mockk(relaxed = true)
+ val storage = PermissionStorage(testContext, this.coroutineContext, sitePermissionsStorage)
+
+ storage.deleteAllSitePermissions()
+
+ verify { sitePermissionsStorage.removeAll() }
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt b/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt
index a9a40ff98..d734256ba 100644
--- a/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/components/TrackingProtectionPolicyFactoryTest.kt
@@ -13,7 +13,6 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.Config
-import org.mozilla.fenix.FeatureFlags
import org.mozilla.fenix.R
import org.mozilla.fenix.ReleaseChannel
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
@@ -346,7 +345,8 @@ class TrackingProtectionPolicyFactoryTest {
shouldBlockCookiesInCustom = true,
blockTrackingContent = false,
blockFingerprinters = true,
- blockCryptominers = false
+ blockCryptominers = false,
+ blockRedirectTrackers = true
)
)
val actual = factory.createTrackingProtectionPolicy(normalMode = true, privateMode = true)
@@ -359,7 +359,7 @@ class TrackingProtectionPolicyFactoryTest {
val expected = TrackingProtectionPolicy.select(
cookiePolicy = TrackingProtectionPolicy.CookiePolicy.ACCEPT_NONE,
trackingCategories = allTrackingCategories,
- cookiePurging = FeatureFlags.etpCookiePurging
+ cookiePurging = true
)
val factory = TrackingProtectionPolicyFactory(settingsForCustom(shouldBlockCookiesInCustom = true))
@@ -421,7 +421,8 @@ private fun settingsForCustom(
blockCookiesSelection: String = "all", // values from R.array.cookies_options_entry_values
blockTrackingContent: Boolean = true,
blockFingerprinters: Boolean = true,
- blockCryptominers: Boolean = true
+ blockCryptominers: Boolean = true,
+ blockRedirectTrackers: Boolean = true
): Settings = mockSettings(useStrict = false, useCustom = true).apply {
every { blockTrackingContentSelectionInCustomTrackingProtection } returns blockTrackingContentInCustom
@@ -431,6 +432,7 @@ private fun settingsForCustom(
every { blockTrackingContentInCustomTrackingProtection } returns blockTrackingContent
every { blockFingerprintersInCustomTrackingProtection } returns blockFingerprinters
every { blockCryptominersInCustomTrackingProtection } returns blockCryptominers
+ every { blockRedirectTrackersInCustomTrackingProtection } returns blockRedirectTrackers
}
private fun TrackingProtectionPolicy.assertPolicyEquals(
diff --git a/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterControllerTest.kt b/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterControllerTest.kt
index d3ac958f6..27fef2732 100644
--- a/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/crashes/CrashReporterControllerTest.kt
@@ -9,7 +9,6 @@ import androidx.navigation.NavDestination
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
-import mozilla.components.browser.session.Session
import mozilla.components.lib.crash.Crash
import mozilla.components.support.test.ext.joinBlocking
import org.junit.Before
@@ -23,7 +22,7 @@ class CrashReporterControllerTest {
private lateinit var components: Components
private lateinit var crash: Crash
- private lateinit var session: Session
+ private lateinit var sessionId: String
private lateinit var navContoller: NavController
private lateinit var settings: Settings
@@ -31,7 +30,7 @@ class CrashReporterControllerTest {
fun setup() {
components = mockk(relaxed = true)
crash = mockk()
- session = mockk()
+ sessionId = "testId"
navContoller = mockk(relaxed = true)
settings = mockk()
@@ -42,14 +41,14 @@ class CrashReporterControllerTest {
@Test
fun `reports crash reporter opened`() {
- CrashReporterController(crash, session, navContoller, components, settings)
+ CrashReporterController(crash, sessionId, navContoller, components, settings)
verify { components.analytics.metrics.track(Event.CrashReporterOpened) }
}
@Test
fun `handle close and restore tab`() {
- val controller = CrashReporterController(crash, session, navContoller, components, settings)
+ val controller = CrashReporterController(crash, sessionId, navContoller, components, settings)
controller.handleCloseAndRestore(sendCrash = false)?.joinBlocking()
verify { components.analytics.metrics.track(Event.CrashReporterClosed(false)) }
@@ -59,11 +58,11 @@ class CrashReporterControllerTest {
@Test
fun `handle close and remove tab`() {
- val controller = CrashReporterController(crash, session, navContoller, components, settings)
+ val controller = CrashReporterController(crash, sessionId, navContoller, components, settings)
controller.handleCloseAndRemove(sendCrash = false)?.joinBlocking()
verify { components.analytics.metrics.track(Event.CrashReporterClosed(false)) }
- verify { components.useCases.tabsUseCases.removeTab(session) }
+ verify { components.useCases.tabsUseCases.removeTab(sessionId) }
verify { components.useCases.sessionUseCases.crashRecovery.invoke() }
verify {
navContoller.navigate(CrashReporterFragmentDirections.actionGlobalHome(), null)
@@ -74,7 +73,7 @@ class CrashReporterControllerTest {
fun `don't submit report if setting is turned off`() {
every { settings.isCrashReportingEnabled } returns false
- val controller = CrashReporterController(crash, session, navContoller, components, settings)
+ val controller = CrashReporterController(crash, sessionId, navContoller, components, settings)
controller.handleCloseAndRestore(sendCrash = true)?.joinBlocking()
verify { components.analytics.metrics.track(Event.CrashReporterClosed(false)) }
@@ -84,7 +83,7 @@ class CrashReporterControllerTest {
fun `submit report if setting is turned on`() {
every { settings.isCrashReportingEnabled } returns true
- val controller = CrashReporterController(crash, session, navContoller, components, settings)
+ val controller = CrashReporterController(crash, sessionId, navContoller, components, settings)
controller.handleCloseAndRestore(sendCrash = true)?.joinBlocking()
verify { components.analytics.crashReporter.submitReport(crash) }
diff --git a/app/src/test/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenuTest.kt b/app/src/test/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenuTest.kt
new file mode 100644
index 000000000..5795d1655
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/customtabs/CustomTabToolbarMenuTest.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.customtabs
+
+import android.content.Context
+import io.mockk.mockk
+import io.mockk.spyk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.CustomTabSessionState
+import mozilla.components.browser.state.state.createCustomTab
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+
+@ExperimentalCoroutinesApi
+class CustomTabToolbarMenuTest {
+
+ private lateinit var firefoxCustomTab: CustomTabSessionState
+ private lateinit var store: BrowserStore
+ private lateinit var customTabToolbarMenu: CustomTabToolbarMenu
+ private lateinit var context: Context
+
+ @Before
+ fun setUp() {
+ context = mockk(relaxed = true)
+
+ firefoxCustomTab = createCustomTab(url = "https://firefox.com", id = "123")
+
+ store = BrowserStore(
+ BrowserState(
+ tabs = listOf(
+ createTab(url = "https://wikipedia.com", id = "1")
+ ),
+ customTabs = listOf(
+ firefoxCustomTab,
+ createCustomTab(url = "https://mozilla.com", id = "456")
+ )
+ )
+ )
+
+ customTabToolbarMenu = spyk(
+ CustomTabToolbarMenu(
+ context = context,
+ store = store,
+ sessionId = firefoxCustomTab.id,
+ shouldReverseItems = false,
+ onItemTapped = { }
+ )
+ )
+ }
+
+ @Test
+ fun `custom tab toolbar menu uses the proper custom tab session`() {
+ assertEquals(firefoxCustomTab.id, customTabToolbarMenu.session?.id)
+ assertEquals("https://firefox.com", customTabToolbarMenu.session?.content?.url)
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessorTest.kt
new file mode 100644
index 000000000..6e9f7c715
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/customtabs/FennecWebAppIntentProcessorTest.kt
@@ -0,0 +1,73 @@
+/* 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.customtabs
+
+import io.mockk.mockk
+import mozilla.components.browser.session.SessionManager
+import mozilla.components.feature.pwa.ManifestStorage
+import mozilla.components.feature.session.SessionUseCases
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import java.io.File
+
+@RunWith(FenixRobolectricTestRunner::class)
+class FennecWebAppIntentProcessorTest {
+ @Test
+ fun `fennec manifest path - tmp`() {
+ val processor = createFennecWebAppIntentProcessor()
+
+ val file = File("/data/local/tmp/dummy_manifest.json")
+ assertFalse(processor.isUnderFennecManifestDirectory(file))
+ }
+
+ @Test
+ fun `fennec manifest path - correct path`() {
+ val processor = createFennecWebAppIntentProcessor()
+
+ val file = File(testContext.filesDir.absolutePath + "/mozilla/rkgl5eyc.default/manifests/c311ad28-f331-482f-ba8f-a0fbf2c56a0d.json")
+ assertTrue(processor.isUnderFennecManifestDirectory(file))
+ }
+
+ @Test
+ fun `fennec manifest path - correct path, but other app`() {
+ val processor = createFennecWebAppIntentProcessor()
+
+ val file = File("/data/data/org.other.app/files/mozilla/rkgl5eyc.default/manifests/c311ad28-f331-482f-ba8f-a0fbf2c56a0d.json")
+ assertFalse(processor.isUnderFennecManifestDirectory(file))
+ }
+
+ @Test
+ fun `fennec manifest path - root file`() {
+ val processor = createFennecWebAppIntentProcessor()
+
+ val file = File("/c311ad28-f331-482f-ba8f-a0fbf2c56a0d.json")
+ assertFalse(processor.isUnderFennecManifestDirectory(file))
+ }
+
+ @Test
+ fun `fennec manifest path - tmp path rebuild`() {
+ val processor = createFennecWebAppIntentProcessor()
+
+ val file = File("/data/local/tmp/files/mozilla/rkgl5eyc.default/manifests/c311ad28-f331-482f-ba8f-a0fbf2c56a0d.json")
+ assertFalse(processor.isUnderFennecManifestDirectory(file))
+ }
+}
+
+private fun createFennecWebAppIntentProcessor(): FennecWebAppIntentProcessor {
+ val sessionManager = SessionManager(engine = mockk(relaxed = true))
+ val useCase: SessionUseCases.DefaultLoadUrlUseCase = mockk(relaxed = true)
+ val storage: ManifestStorage = mockk(relaxed = true)
+
+ return FennecWebAppIntentProcessor(
+ testContext,
+ sessionManager,
+ useCase,
+ storage
+ )
+}
diff --git a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt
index 28211359f..6025ba8b3 100644
--- a/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/home/DefaultSessionControlControllerTest.kt
@@ -8,20 +8,30 @@ import androidx.navigation.NavController
import androidx.navigation.NavDirections
import io.mockk.every
import io.mockk.mockk
+import io.mockk.mockkStatic
+import io.mockk.spyk
+import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
-import mozilla.components.browser.session.SessionManager
+import mozilla.components.browser.state.action.SearchAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.search.RegionState
import mozilla.components.browser.state.search.SearchEngine
import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.ReaderState
import mozilla.components.browser.state.state.SearchState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
+import mozilla.components.browser.state.state.selectedOrDefaultSearchEngine
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.engine.Engine
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.tab.collections.TabCollection
import mozilla.components.feature.tabs.TabsUseCases
import mozilla.components.feature.top.sites.TopSite
+import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
import org.junit.Before
@@ -33,6 +43,7 @@ import org.mozilla.fenix.R
import org.mozilla.fenix.components.Analytics
import org.mozilla.fenix.components.TabCollectionStorage
import org.mozilla.fenix.components.metrics.Event
+import org.mozilla.fenix.components.metrics.Event.PerformedSearch.EngineSource
import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.components.tips.Tip
import org.mozilla.fenix.ext.components
@@ -54,11 +65,11 @@ class DefaultSessionControlControllerTest {
private val fragmentStore: HomeFragmentStore = mockk(relaxed = true)
private val navController: NavController = mockk(relaxed = true)
private val metrics: MetricController = mockk(relaxed = true)
- private val sessionManager: SessionManager = mockk(relaxed = true)
private val engine: Engine = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
private val reloadUrlUseCase: SessionUseCases = mockk(relaxed = true)
+ private val selectTabUseCase: TabsUseCases = mockk(relaxed = true)
private val hideOnboarding: () -> Unit = mockk(relaxed = true)
private val registerCollectionStorageObserver: () -> Unit = mockk(relaxed = true)
private val showTabTray: () -> Unit = mockk(relaxed = true)
@@ -80,16 +91,28 @@ class DefaultSessionControlControllerTest {
type = SearchEngine.Type.BUNDLED,
resultUrls = listOf("https://example.org/?q={searchTerms}")
)
+
+ private val googleSearchEngine = SearchEngine(
+ id = "googleTest",
+ name = "Google Test Engine",
+ icon = mockk(relaxed = true),
+ type = SearchEngine.Type.BUNDLED,
+ resultUrls = listOf("https://www.google.com/?q={searchTerms}"),
+ suggestUrl = "https://www.google.com/"
+ )
+
private lateinit var store: BrowserStore
private lateinit var controller: DefaultSessionControlController
@Before
fun setup() {
- store = BrowserStore(BrowserState(
- search = SearchState(
- regionSearchEngines = listOf(searchEngine)
+ store = BrowserStore(
+ BrowserState(
+ search = SearchState(
+ regionSearchEngines = listOf(searchEngine)
+ )
)
- ))
+ )
every { fragmentStore.state } returns HomeFragmentState(
collections = emptyList(),
@@ -99,7 +122,6 @@ class DefaultSessionControlControllerTest {
showCollectionPlaceholder = true
)
- every { sessionManager.sessions } returns emptyList()
every { navController.currentDestination } returns mockk {
every { id } returns R.id.homeFragment
}
@@ -108,16 +130,19 @@ class DefaultSessionControlControllerTest {
every { activity.components.analytics } returns analytics
every { analytics.metrics } returns metrics
- controller = DefaultSessionControlController(
+ val restoreUseCase: TabsUseCases.RestoreUseCase = mockk(relaxed = true)
+
+ controller = spyk(DefaultSessionControlController(
activity = activity,
store = store,
settings = settings,
engine = engine,
metrics = metrics,
- sessionManager = sessionManager,
tabCollectionStorage = tabCollectionStorage,
addTabUseCase = tabsUseCases.addTab,
reloadUrlUseCase = reloadUrlUseCase.reload,
+ selectTabUseCase = selectTabUseCase.selectTab,
+ restoreUseCase = restoreUseCase,
fragmentStore = fragmentStore,
navController = navController,
viewLifecycleScope = scope,
@@ -126,7 +151,7 @@ class DefaultSessionControlControllerTest {
showDeleteCollectionPrompt = showDeleteCollectionPrompt,
showTabTray = showTabTray,
handleSwipedItemDeletionCancel = handleSwipedItemDeletionCancel
- )
+ ))
}
@After
@@ -172,18 +197,62 @@ class DefaultSessionControlControllerTest {
}
@Test
- fun `handleCollectionOpenTabClicked onTabRestored`() {
+ fun `handleCollectionOpenTabClicked with existing selected tab`() {
+ val recoverableTab = RecoverableTab(
+ id = "test",
+ parentId = null,
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ state = null,
+ contextId = null,
+ readerState = ReaderState(),
+ lastAccess = 0,
+ private = false
+ )
+
val tab = mockk {
- every { restore(activity, engine, restoreSessionId = false) } returns mockk {
- every { session } returns mockk()
- every { engineSessionState } returns mockk()
- }
+ every { restore(activity, engine, restoreSessionId = false) } returns recoverableTab
}
+
+ val restoredTab = createTab(id = recoverableTab.id, url = recoverableTab.url)
+ val otherTab = createTab(id = "otherTab", url = "https://mozilla.org")
+ store.dispatch(TabListAction.AddTabAction(otherTab)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(otherTab.id)).joinBlocking()
+ store.dispatch(TabListAction.AddTabAction(restoredTab)).joinBlocking()
+
controller.handleCollectionOpenTabClicked(tab)
+ verify { metrics.track(Event.CollectionTabRestored) }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ verify { selectTabUseCase.selectTab.invoke(restoredTab.id) }
+ verify { reloadUrlUseCase.reload.invoke(restoredTab.id) }
+ }
+
+ @Test
+ fun `handleCollectionOpenTabClicked without existing selected tab`() {
+ val recoverableTab = RecoverableTab(
+ id = "test",
+ parentId = null,
+ url = "https://www.mozilla.org",
+ title = "Mozilla",
+ state = null,
+ contextId = null,
+ readerState = ReaderState(),
+ lastAccess = 0,
+ private = false
+ )
+
+ val tab = mockk {
+ every { restore(activity, engine, restoreSessionId = false) } returns recoverableTab
+ }
+ val restoredTab = createTab(id = recoverableTab.id, url = recoverableTab.url)
+ store.dispatch(TabListAction.AddTabAction(restoredTab)).joinBlocking()
+
+ controller.handleCollectionOpenTabClicked(tab)
verify { metrics.track(Event.CollectionTabRestored) }
verify { activity.openToBrowser(BrowserDirection.FromHome) }
- verify { reloadUrlUseCase.reload }
+ verify { selectTabUseCase.selectTab.invoke(restoredTab.id) }
+ verify { reloadUrlUseCase.reload.invoke(restoredTab.id) }
}
@Test
@@ -335,6 +404,158 @@ class DefaultSessionControlControllerTest {
verify { activity.openToBrowser(BrowserDirection.FromHome) }
}
+ @Test
+ fun handleSelectGoogleDefaultTopSiteUS() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenDefault) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ url = SupportUtils.GOOGLE_US_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
+ @Test
+ fun handleSelectGoogleDefaultTopSiteXX() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenDefault) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ SupportUtils.GOOGLE_XX_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
+ @Test
+ fun handleSelectGoogleDefaultTopSite_EventPerformedSearchTopSite() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ val engineSource = EngineSource.Default(googleSearchEngine, false)
+ every { controller.getAvailableSearchEngines() } returns listOf(googleSearchEngine)
+ try {
+ mockkStatic("mozilla.components.browser.state.state.SearchStateKt")
+
+ every { any().selectedOrDefaultSearchEngine } returns googleSearchEngine
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.DEFAULT)
+
+ verify {
+ metrics.track(
+ Event.PerformedSearch(
+ Event.PerformedSearch.EventSource.TopSite(
+ engineSource
+ )
+ )
+ )
+ }
+ } finally {
+ unmockkStatic(SearchState::class)
+ }
+ }
+
+ @Test
+ fun handleSelectGooglePinnedTopSiteUS() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.PINNED)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenPinned) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ SupportUtils.GOOGLE_US_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
+ @Test
+ fun handleSelectGooglePinnedTopSiteXX() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.PINNED)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenPinned) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ SupportUtils.GOOGLE_XX_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
+ @Test
+ fun handleSelectGoogleFrecentTopSiteUS() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("US", "US"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.FRECENT)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenFrecent) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ SupportUtils.GOOGLE_US_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
+ @Test
+ fun handleSelectGoogleFrecentTopSiteXX() {
+ val topSiteUrl = SupportUtils.GOOGLE_URL
+ every { controller.getAvailableSearchEngines() } returns listOf(searchEngine)
+
+ store.dispatch(SearchAction.SetRegionAction(RegionState("DE", "FR"))).joinBlocking()
+
+ controller.handleSelectTopSite(topSiteUrl, TopSite.Type.FRECENT)
+ verify { metrics.track(Event.TopSiteOpenInNewTab) }
+ verify { metrics.track(Event.TopSiteOpenFrecent) }
+ verify { metrics.track(Event.TopSiteOpenGoogle) }
+ verify {
+ tabsUseCases.addTab.invoke(
+ SupportUtils.GOOGLE_XX_URL,
+ selectTab = true,
+ startLoading = true
+ )
+ }
+ verify { activity.openToBrowser(BrowserDirection.FromHome) }
+ }
+
@Test
fun handleStartBrowsingClicked() {
controller.handleStartBrowsingClicked()
diff --git a/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt
new file mode 100644
index 000000000..8f24fa8e8
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/home/HomeFragmentTest.kt
@@ -0,0 +1,68 @@
+/* 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
+
+import android.content.Context
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.ext.settings
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.utils.Settings
+
+@ExperimentalCoroutinesApi
+@RunWith(FenixRobolectricTestRunner::class)
+class HomeFragmentTest {
+
+ private lateinit var settings: Settings
+ private lateinit var context: Context
+ private lateinit var homeFragment: HomeFragment
+
+ @Before
+ fun setup() {
+ context = mockk(relaxed = true)
+ settings = mockk(relaxed = true)
+
+ homeFragment = spyk(HomeFragment())
+
+ every { homeFragment.context } returns context
+ every { context.settings() } returns settings
+ }
+
+ @Test
+ fun `GIVEN showTopFrecentSites is false WHEN getTopSitesConfig is called THEN it returns TopSitesConfig with null frecencyConfig`() {
+ every { settings.showTopFrecentSites } returns false
+ every { settings.topSitesMaxLimit } returns 10
+
+ val topSitesConfig = homeFragment.getTopSitesConfig()
+
+ Assert.assertNull(topSitesConfig.frecencyConfig)
+ }
+
+ @Test
+ fun `GIVEN showTopFrecentSites is true WHEN getTopSitesConfig is called THEN it returns TopSitesConfig with non-null frecencyConfig`() {
+ every { context.settings().showTopFrecentSites } returns true
+ every { settings.topSitesMaxLimit } returns 10
+
+ val topSitesConfig = homeFragment.getTopSitesConfig()
+
+ Assert.assertNotNull(topSitesConfig.frecencyConfig)
+ }
+
+ @Test
+ fun `GIVEN a topSitesMaxLimit WHEN getTopSitesConfig is called THEN it returns TopSitesConfig with totalSites = topSitesMaxLimit`() {
+ val topSitesMaxLimit = 10
+ every { settings.topSitesMaxLimit } returns topSitesMaxLimit
+
+ val topSitesConfig = homeFragment.getTopSitesConfig()
+
+ Assert.assertEquals(topSitesMaxLimit, topSitesConfig.totalSites)
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt
index 7dc14cc99..ea155c2fc 100644
--- a/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/home/intent/OpenSpecificTabIntentProcessorTest.kt
@@ -11,10 +11,12 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import io.mockk.verifyOrder
-import mozilla.components.browser.session.Session
-import mozilla.components.browser.session.SessionManager
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.media.service.AbstractMediaService
import mozilla.components.feature.media.service.AbstractMediaSessionService
+import mozilla.components.feature.tabs.TabsUseCases
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
@@ -42,7 +44,7 @@ class OpenSpecificTabIntentProcessorTest {
}
@Test
- fun `GIVEN a blank intent, WHEN it is processed, THEN nothing should happen`() {
+ fun `GIVEN a blank intent WHEN it is processed THEN nothing should happen`() {
assertFalse(processor.process(Intent(), navController, out))
verify { activity wasNot Called }
@@ -51,7 +53,7 @@ class OpenSpecificTabIntentProcessorTest {
}
@Test
- fun `GIVEN an intent with wrong action, WHEN it is processed, THEN nothing should happen`() {
+ fun `GIVEN an intent with wrong action WHEN it is processed THEN nothing should happen`() {
val intent = Intent().apply {
action = TEST_WRONG_ACTION
}
@@ -64,11 +66,10 @@ class OpenSpecificTabIntentProcessorTest {
}
@Test
- fun `GIVEN an intent with null extra string, WHEN it is processed, THEN openToBrowser should not be called`() {
+ fun `GIVEN an intent with null extra string WHEN it is processed THEN openToBrowser should not be called`() {
val intent = Intent().apply {
action = AbstractMediaService.Companion.ACTION_SWITCH_TAB
}
- every { activity.components.core.sessionManager } returns mockk(relaxed = true)
assertFalse(processor.process(intent, navController, out))
@@ -78,7 +79,7 @@ class OpenSpecificTabIntentProcessorTest {
}
@Test
- fun `GIVEN an intent with correct action and extra string, WHEN it is processed, THEN session should be selected and openToBrowser should be called`() {
+ fun `GIVEN an intent with correct action and extra string WHEN it is processed THEN session should be selected and openToBrowser should be called`() {
val intent = Intent().apply {
if (newMediaSessionApi) {
action = AbstractMediaSessionService.Companion.ACTION_SWITCH_TAB
@@ -88,15 +89,15 @@ class OpenSpecificTabIntentProcessorTest {
putExtra(AbstractMediaService.Companion.EXTRA_TAB_ID, TEST_SESSION_ID)
}
}
- val sessionManager: SessionManager = mockk(relaxed = true)
- val session: Session = mockk(relaxed = true)
- every { activity.components.core.sessionManager } returns sessionManager
- every { sessionManager.findSessionById(TEST_SESSION_ID) } returns session
+ val store = BrowserStore(BrowserState(tabs = listOf(createTab(id = TEST_SESSION_ID, url = "https:mozilla.org"))))
+ val tabUseCases: TabsUseCases = mockk(relaxed = true)
+ every { activity.components.core.store } returns store
+ every { activity.components.useCases.tabsUseCases } returns tabUseCases
assertTrue(processor.process(intent, navController, out))
verifyOrder {
- sessionManager.select(session)
+ tabUseCases.selectTab(TEST_SESSION_ID)
activity.openToBrowser(BrowserDirection.FromGlobal)
}
verify { navController wasNot Called }
diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapterTest.kt
new file mode 100644
index 000000000..b64a0130d
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesAdapterTest.kt
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites
+
+import mozilla.components.feature.top.sites.TopSite
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TopSitesAdapterTest {
+
+ @Test
+ fun testDiffCallback() {
+ val topSite = TopSite(
+ id = 1L,
+ title = "Title1",
+ url = "https://mozilla.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+ val topSite2 = TopSite(
+ id = 1L,
+ title = "Title2",
+ url = "https://mozilla.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+
+ assertEquals(
+ TopSitesAdapter.TopSitesDiffCallback.getChangePayload(topSite, topSite2),
+ TopSitesAdapter.TopSitePayload("Title2")
+ )
+
+ val topSite3 = TopSite(
+ id = 2L,
+ title = "Title2",
+ url = "https://firefox.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+
+ assertEquals(
+ TopSitesAdapter.TopSitesDiffCallback.getChangePayload(topSite, topSite3),
+ null
+ )
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapterTest.kt b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapterTest.kt
new file mode 100644
index 000000000..d8d26bf17
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/home/sessioncontrol/viewholders/topsites/TopSitesPagerAdapterTest.kt
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.fenix.home.sessioncontrol.viewholders.topsites
+
+import mozilla.components.feature.top.sites.TopSite
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.mozilla.fenix.home.sessioncontrol.AdapterItem
+
+class TopSitesPagerAdapterTest {
+ @Test
+ fun testDiffCallback() {
+ val topSite = TopSite(
+ id = 1L,
+ title = "Title1",
+ url = "https://mozilla.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+
+ val topSite2 = TopSite(
+ id = 2L,
+ title = "Title2",
+ url = "https://mozilla.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+
+ val topSite3 = TopSite(
+ id = 3L,
+ title = "Title2",
+ url = "https://firefox.org",
+ null,
+ TopSite.Type.DEFAULT
+ )
+
+ assertEquals(
+ TopSitesPagerAdapter.TopSiteListDiffCallback.getChangePayload(
+ listOf(topSite, topSite3),
+ listOf(topSite, topSite2)
+ ),
+ AdapterItem.TopSitePagerPayload(setOf(Pair(1, topSite2)))
+ )
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt
index 1d2a767a1..98825ea99 100644
--- a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/DefaultRecentlyClosedControllerTest.kt
@@ -13,18 +13,15 @@ import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
-import io.mockk.mockkStatic
import io.mockk.slot
-import io.mockk.unmockkStatic
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
-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.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
@@ -47,16 +44,16 @@ class DefaultRecentlyClosedControllerTest {
private val resources: Resources = mockk(relaxed = true)
private val snackbar: FenixSnackbar = mockk(relaxed = true)
private val clipboardManager: ClipboardManager = mockk(relaxed = true)
- private val openToBrowser: (ClosedTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
- private val sessionManager: SessionManager = mockk(relaxed = true)
+ private val openToBrowser: (RecoverableTab, BrowsingMode?) -> Unit = mockk(relaxed = true)
private val activity: HomeActivity = mockk(relaxed = true)
private val store: BrowserStore = mockk(relaxed = true)
- val mockedTab: ClosedTab = mockk(relaxed = true)
+ private val tabsUseCases: TabsUseCases = mockk(relaxed = true)
+ val mockedTab: RecoverableTab = mockk(relaxed = true)
private val controller = DefaultRecentlyClosedController(
navController,
store,
- sessionManager,
+ tabsUseCases,
resources,
snackbar,
clipboardManager,
@@ -66,19 +63,17 @@ class DefaultRecentlyClosedControllerTest {
@Before
fun setUp() {
- mockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
- every { mockedTab.restoreTab(any(), any(), any()) } just Runs
+ every { tabsUseCases.restore.invoke(any(), true) } just Runs
}
@After
fun tearDown() {
dispatcher.cleanupTestCoroutines()
- unmockkStatic("mozilla.components.feature.recentlyclosed.ext.ClosedTabKt")
}
@Test
fun handleOpen() {
- val item: ClosedTab = mockk(relaxed = true)
+ val item: RecoverableTab = mockk(relaxed = true)
controller.handleOpen(item, BrowsingMode.Private)
@@ -95,7 +90,7 @@ class DefaultRecentlyClosedControllerTest {
@Test
fun handleDeleteOne() {
- val item: ClosedTab = mockk(relaxed = true)
+ val item: RecoverableTab = mockk(relaxed = true)
controller.handleDeleteOne(item)
@@ -120,7 +115,7 @@ class DefaultRecentlyClosedControllerTest {
@Test
fun handleCopyUrl() {
- val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val item = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
val clipdata = slot()
@@ -139,7 +134,7 @@ class DefaultRecentlyClosedControllerTest {
@Test
@Suppress("UNCHECKED_CAST")
fun handleShare() {
- val item = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val item = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
controller.handleShare(item)
@@ -160,12 +155,6 @@ class DefaultRecentlyClosedControllerTest {
dispatcher.advanceUntilIdle()
- verify {
- mockedTab.restoreTab(
- store,
- sessionManager,
- onTabRestored = any()
- )
- }
+ verify { tabsUseCases.restore.invoke(mockedTab, true) }
}
}
diff --git a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt
index c4242bc03..934ce58ea 100644
--- a/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/library/recentlyclosed/RecentlyClosedFragmentInteractorTest.kt
@@ -6,7 +6,7 @@ package org.mozilla.fenix.library.recentlyclosed
import io.mockk.mockk
import io.mockk.verify
-import mozilla.components.browser.state.state.ClosedTab
+import mozilla.components.browser.state.state.recover.RecoverableTab
import org.junit.Before
import org.junit.Test
import org.mozilla.fenix.browser.browsingmode.BrowsingMode
@@ -27,7 +27,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun open() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.restore(tab)
verify {
@@ -37,7 +37,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun onCopyPressed() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onCopyPressed(tab)
verify {
@@ -47,7 +47,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun onSharePressed() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onSharePressed(tab)
verify {
@@ -57,7 +57,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun onOpenInNormalTab() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onOpenInNormalTab(tab)
verify {
@@ -67,7 +67,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun onOpenInPrivateTab() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onOpenInPrivateTab(tab)
verify {
@@ -77,7 +77,7 @@ class RecentlyClosedFragmentInteractorTest {
@Test
fun onDeleteOne() {
- val tab = ClosedTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", createdAt = 1L)
+ val tab = RecoverableTab(id = "tab-id", title = "Mozilla", url = "mozilla.org", lastAccess = 1L)
interactor.onDeleteOne(tab)
verify {
diff --git a/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt
new file mode 100644
index 000000000..59698c783
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/perf/PerformanceInflaterTest.kt
@@ -0,0 +1,84 @@
+/* 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.widget.FrameLayout
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import java.io.File
+
+@RunWith(FenixRobolectricTestRunner::class)
+class PerformanceInflaterTest {
+
+ private lateinit var perfInflater: MockInflater
+
+ private val layoutsNotToTest = setOf(
+ "fragment_browser",
+ "fragment_add_on_internal_settings"
+ )
+
+ @Before
+ fun setup() {
+ InflationCounter.inflationCount.set(0)
+
+ perfInflater = MockInflater(LayoutInflater.from(testContext), testContext)
+ }
+
+ @Test
+ fun `WHEN we inflate a view,THEN the inflation counter should increase`() {
+ assertEquals(0, InflationCounter.inflationCount.get())
+ perfInflater.inflate(R.layout.fragment_home, null, false)
+ assertEquals(1, InflationCounter.inflationCount.get())
+ }
+
+ @Test
+ fun `WHEN inflating one of our resource file, the inflater should not crash`() {
+ val fileList = File("./src/main/res/layout").listFiles()
+ for (file in fileList!!) {
+ val layoutName = file.name.split(".")[0]
+ val layoutId = testContext.resources.getIdentifier(
+ layoutName,
+ "layout",
+ testContext.packageName
+ )
+
+ assertNotEquals(-1, layoutId)
+ if (!layoutsNotToTest.contains(layoutName)) {
+ perfInflater.inflate(layoutId, FrameLayout(testContext), true)
+ }
+ }
+ }
+}
+
+private class MockInflater(
+ inflater: LayoutInflater,
+ context: Context
+) : PerformanceInflater(
+ inflater,
+ context
+) {
+
+ override fun onCreateView(name: String?, attrs: AttributeSet?): View? {
+ // We skip the fragment layout for the simple reason that it implements
+ // a whole different inflate which is implemented in the activity.LayoutFactory
+ // methods. To be able to properly test it here, we would have to copy the whole
+ // inflater file (or create an activity) and pass our layout through the onCreateView
+ // method of that activity.
+ if (name!!.contains("fragment")) {
+ return FrameLayout(testContext)
+ }
+ return super.onCreateView(name, attrs)
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt b/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt
index 52cbea14a..f8b79a052 100644
--- a/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/search/SearchDialogControllerTest.kt
@@ -23,6 +23,7 @@ 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 org.junit.After
import org.junit.Before
import org.junit.Test
@@ -68,10 +69,12 @@ class SearchDialogControllerTest {
}
every { MetricsUtils.createSearchEvent(searchEngine, browserStore, any()) } returns null
+ val tabsUseCases = TabsUseCases(browserStore, sessionManager)
+
controller = SearchDialogController(
activity = activity,
- sessionManager = sessionManager,
store = browserStore,
+ tabsUseCases = tabsUseCases,
fragmentStore = store,
navController = navController,
settings = settings,
@@ -324,7 +327,9 @@ class SearchDialogControllerTest {
fun handleExistingSessionSelected() {
val session = mockk()
- controller.handleExistingSessionSelected(session)
+ every { sessionManager.findSessionById("selected") } returns session
+
+ controller.handleExistingSessionSelected("selected")
verify { sessionManager.select(session) }
verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
@@ -337,7 +342,7 @@ class SearchDialogControllerTest {
controller.handleExistingSessionSelected("tab-id")
verify(inverse = true) { sessionManager.select(any()) }
- verify(inverse = true) { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
+ verify { activity.openToBrowser(from = BrowserDirection.FromSearchDialog) }
}
@Test
diff --git a/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt
index 53a224c19..77014b1c2 100644
--- a/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/search/SearchDialogInteractorTest.kt
@@ -100,10 +100,10 @@ class SearchDialogInteractorTest {
fun onExistingSessionSelected() {
val session = Session("http://mozilla.org", false)
- interactor.onExistingSessionSelected(session)
+ interactor.onExistingSessionSelected(session.id)
verify {
- searchController.handleExistingSessionSelected(session)
+ searchController.handleExistingSessionSelected(session.id)
}
}
diff --git a/app/src/test/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetryTest.kt b/app/src/test/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetryTest.kt
index b491df390..36e30f6cd 100644
--- a/app/src/test/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetryTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/search/telemetry/incontent/InContentTelemetryTest.kt
@@ -109,6 +109,15 @@ class InContentTelemetryTest {
verify { metrics.track(Event.SearchInContent("google.in-content.sap-follow-on.firefox-b-m")) }
}
+ @Test
+ fun `track google sap-follow-on and topSite metric`() {
+ val url = "https://www.google.com/search?q=aaa&client=firefox-b-m&channel=ts&oq=random"
+
+ telemetry.trackPartnerUrlTypeMetric(url, listOf())
+
+ verify { metrics.track(Event.SearchInContent("google.in-content.sap-follow-on.firefox-b-m.ts")) }
+ }
+
@Test
fun `track baidu sap-follow-on metric`() {
val url = "https://www.baidu.com/from=844b/s?wd=aaa&tn=34046034_firefox&oq=random"
diff --git a/app/src/test/java/org/mozilla/fenix/settings/PhoneFeatureTest.kt b/app/src/test/java/org/mozilla/fenix/settings/PhoneFeatureTest.kt
index 4980d5080..f378a174c 100644
--- a/app/src/test/java/org/mozilla/fenix/settings/PhoneFeatureTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/settings/PhoneFeatureTest.kt
@@ -65,6 +65,7 @@ class PhoneFeatureTest {
assertEquals("Notification", PhoneFeature.NOTIFICATION.getLabel(testContext))
assertEquals("Autoplay", PhoneFeature.AUTOPLAY_AUDIBLE.getLabel(testContext))
assertEquals("Autoplay", PhoneFeature.AUTOPLAY_INAUDIBLE.getLabel(testContext))
+ assertEquals("Autoplay", PhoneFeature.AUTOPLAY.getLabel(testContext))
}
@Test
@@ -75,6 +76,7 @@ class PhoneFeatureTest {
assertEquals(R.string.pref_key_phone_feature_notification, PhoneFeature.NOTIFICATION.getPreferenceId())
assertEquals(R.string.pref_key_browser_feature_autoplay_audible, PhoneFeature.AUTOPLAY_AUDIBLE.getPreferenceId())
assertEquals(R.string.pref_key_browser_feature_autoplay_inaudible, PhoneFeature.AUTOPLAY_INAUDIBLE.getPreferenceId())
+ assertEquals(R.string.pref_key_browser_feature_autoplay_audible, PhoneFeature.AUTOPLAY.getPreferenceId())
assertEquals(
"pref_key_browser_feature_autoplay_inaudible",
diff --git a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt
index 414c8dfd4..328f5ffb3 100644
--- a/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/settings/deletebrowsingdata/DefaultDeleteBrowsingDataControllerTest.kt
@@ -13,10 +13,12 @@ import kotlinx.coroutines.GlobalScope.coroutineContext
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.runBlockingTest
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 mozilla.components.support.test.rule.MainCoroutineRule
import org.junit.After
@@ -34,6 +36,7 @@ class DefaultDeleteBrowsingDataControllerTest {
val coroutinesTestRule = MainCoroutineRule(testDispatcher)
private var removeAllTabs: TabsUseCases.RemoveAllTabsUseCase = mockk(relaxed = true)
+ private var removeAllDownloads: DownloadsUseCases.RemoveAllDownloadsUseCase = mockk(relaxed = true)
private var historyStorage: HistoryStorage = mockk(relaxed = true)
private var permissionStorage: PermissionStorage = mockk(relaxed = true)
private var store: BrowserStore = mockk(relaxed = true)
@@ -45,6 +48,7 @@ class DefaultDeleteBrowsingDataControllerTest {
fun setup() {
controller = DefaultDeleteBrowsingDataController(
removeAllTabs = removeAllTabs,
+ removeAllDownloads = removeAllDownloads,
historyStorage = historyStorage,
store = store,
permissionStorage = permissionStorage,
@@ -77,6 +81,7 @@ class DefaultDeleteBrowsingDataControllerTest {
coVerify {
engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES))
historyStorage.deleteEverything()
+ store.dispatch(EngineAction.PurgeHistoryAction)
store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction)
iconsStorage.clear()
}
@@ -115,4 +120,14 @@ class DefaultDeleteBrowsingDataControllerTest {
permissionStorage.deleteAllSitePermissions()
}
}
+
+ @Test
+ fun deleteDownloads() = runBlockingTest {
+
+ controller.deleteDownloads()
+
+ verify {
+ removeAllDownloads.invoke()
+ }
+ }
}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/AutoplayValueTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/AutoplayValueTest.kt
new file mode 100644
index 000000000..34800805c
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/AutoplayValueTest.kt
@@ -0,0 +1,381 @@
+/* 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.settings.quicksettings
+
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+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.feature.sitepermissions.SitePermissionsRules.Action
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertTrue
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.utils.Settings
+
+@RunWith(FenixRobolectricTestRunner::class)
+class AutoplayValueTest {
+ @MockK(relaxed = true)
+ private lateinit var settings: Settings
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ }
+
+ @Test
+ fun `AllowAll - isSelected`() {
+ var rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ var value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+
+ rules = rules.copy(
+ autoplayAudible = AutoplayAction.BLOCKED
+ )
+
+ value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+
+ value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+ )
+
+ assertTrue(value.isSelected())
+
+ value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `BlockAll - isSelected`() {
+ var rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED
+ )
+
+ var value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+
+ rules = rules.copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+
+ value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertTrue(value.isSelected())
+
+ value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `BlockAudible - isSelected`() {
+ var rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ var value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+
+ rules = rules.copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED
+ )
+
+ value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+
+ value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+ )
+
+ assertTrue(value.isSelected())
+
+ value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `AllowAll - createSitePermissionsFromCustomRules`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `BlockAll - createSitePermissionsFromCustomRules`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `BlockAudible - createSitePermissionsFromCustomRules`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `AllowAll - updateSitePermissions`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `BlockAll - updateSitePermissions`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `BlockAudible - updateSitePermissions`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `values - contains the right values`() {
+ val values = AutoplayValue.values(testContext, settings, null)
+
+ assertTrue(values.any { it is AutoplayValue.AllowAll })
+ assertTrue(values.any { it is AutoplayValue.BlockAll })
+ assertTrue(values.any { it is AutoplayValue.BlockAudible })
+ }
+
+ private fun getRules() = SitePermissionsRules(
+ camera = Action.ASK_TO_ALLOW,
+ location = Action.ASK_TO_ALLOW,
+ microphone = Action.ASK_TO_ALLOW,
+ notification = Action.ASK_TO_ALLOW,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ persistentStorage = Action.ASK_TO_ALLOW,
+ mediaKeySystemAccess = Action.ASK_TO_ALLOW
+ )
+}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt
index a8bedf56d..57f7e8d6d 100644
--- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/DefaultQuickSettingsControllerTest.kt
@@ -13,18 +13,25 @@ import io.mockk.just
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
-import mozilla.components.browser.session.Session
+import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.session.SessionUseCases
import mozilla.components.feature.sitepermissions.SitePermissions
import mozilla.components.feature.sitepermissions.SitePermissions.Status.NO_DECISION
import mozilla.components.feature.tabs.TabsUseCases
+import mozilla.components.support.test.mock
import mozilla.components.support.test.robolectric.testContext
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertSame
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.components.PermissionStorage
@@ -39,33 +46,70 @@ import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class DefaultQuickSettingsControllerTest {
private val context = testContext
- private val store = mockk()
+
+ private lateinit var browserStore: BrowserStore
+ private lateinit var tab: TabSessionState
+
+ @MockK
+ private lateinit var store: QuickSettingsFragmentStore
private val coroutinesScope = TestCoroutineScope()
- private val navController = mockk(relaxed = true)
- private val browserSession = mockk()
- private val sitePermissions: SitePermissions = SitePermissions(origin = "", savedAt = 123)
- private val appSettings = mockk(relaxed = true)
- private val permissionStorage = mockk(relaxed = true)
- private val reload = mockk(relaxed = true)
- private val addNewTab = mockk(relaxed = true)
- private val requestPermissions = mockk<(Array) -> Unit>(relaxed = true)
- private val displayPermissions = mockk<() -> Unit>(relaxed = true)
- private val dismiss = mockk<() -> Unit>(relaxed = true)
- private val controller = spyk(DefaultQuickSettingsController(
- context = context,
- quickSettingsStore = store,
- ioScope = coroutinesScope,
- navController = navController,
- session = browserSession,
- sitePermissions = sitePermissions,
- settings = appSettings,
- permissionStorage = permissionStorage,
- reload = reload,
- addNewTab = addNewTab,
- requestRuntimePermissions = requestPermissions,
- displayPermissions = displayPermissions,
- dismiss = dismiss
- ))
+
+ @MockK(relaxed = true)
+ private lateinit var navController: NavController
+
+ @MockK(relaxed = true)
+ private lateinit var sitePermissions: SitePermissions
+
+ @MockK(relaxed = true)
+ private lateinit var appSettings: Settings
+
+ @MockK(relaxed = true)
+ private lateinit var permissionStorage: PermissionStorage
+
+ @MockK(relaxed = true)
+ private lateinit var reload: SessionUseCases.ReloadUrlUseCase
+
+ @MockK(relaxed = true)
+ private lateinit var addNewTab: TabsUseCases.AddNewTabUseCase
+
+ @MockK(relaxed = true)
+ private lateinit var requestPermissions: (Array) -> Unit
+
+ @MockK(relaxed = true)
+ private lateinit var displayPermissions: () -> Unit
+
+ @MockK(relaxed = true)
+ private lateinit var dismiss: () -> Unit
+
+ private lateinit var controller: DefaultQuickSettingsController
+
+ @Before
+ fun setUp() {
+ MockKAnnotations.init(this)
+
+ tab = createTab("https://mozilla.org")
+ browserStore = BrowserStore(BrowserState(tabs = listOf(tab)))
+ sitePermissions = SitePermissions(origin = "", savedAt = 123)
+
+ controller = spyk(
+ DefaultQuickSettingsController(
+ context = context,
+ quickSettingsStore = store,
+ browserStore = browserStore,
+ sessionId = tab.id,
+ ioScope = coroutinesScope,
+ navController = navController,
+ sitePermissions = sitePermissions,
+ settings = appSettings,
+ permissionStorage = permissionStorage,
+ reload = reload,
+ addNewTab = addNewTab,
+ requestRuntimePermissions = requestPermissions,
+ displayPermissions = displayPermissions,
+ dismiss = dismiss
+ )
+ )
+ }
@After
fun cleanUp() {
@@ -123,9 +167,10 @@ class DefaultQuickSettingsControllerTest {
val invalidSitePermissionsController = DefaultQuickSettingsController(
context = context,
quickSettingsStore = store,
+ browserStore = BrowserStore(),
ioScope = coroutinesScope,
navController = navController,
- session = browserSession,
+ sessionId = "123",
sitePermissions = null,
settings = appSettings,
permissionStorage = permissionStorage,
@@ -149,6 +194,39 @@ class DefaultQuickSettingsControllerTest {
}
}
+ @Test
+ fun `handleAutoplayChanged will add autoplay permission`() {
+ val autoplayValue = mockk(relaxed = true)
+
+ every { store.dispatch(any()) } returns mockk()
+ every { controller.handleAutoplayAdd(any()) } returns Unit
+
+ controller.sitePermissions = null
+
+ controller.handleAutoplayChanged(autoplayValue)
+
+ verify {
+ controller.handleAutoplayAdd(any())
+ store.dispatch(any())
+ }
+ }
+
+ @Test
+ fun `handleAutoplayChanged will update autoplay permission`() {
+ val autoplayValue = mockk(relaxed = true)
+
+ every { store.dispatch(any()) } returns mockk()
+ every { controller.handleAutoplayAdd(any()) } returns Unit
+ every { controller.handlePermissionsChange(any()) } returns Unit
+ every { autoplayValue.updateSitePermissions(any()) } returns mock()
+
+ controller.handleAutoplayChanged(autoplayValue)
+
+ verify {
+ autoplayValue.updateSitePermissions(any())
+ store.dispatch(any())
+ }
+ }
@Test
fun `handleAndroidPermissionGranted should update the View's state`() {
val featureGranted = PhoneFeature.CAMERA
@@ -178,7 +256,6 @@ class DefaultQuickSettingsControllerTest {
}
@Test
- @ExperimentalCoroutinesApi
fun `handlePermissionsChange should store the updated permission and reload webpage`() = coroutinesScope.runBlockingTest {
val testPermissions = mockk()
@@ -187,7 +264,20 @@ class DefaultQuickSettingsControllerTest {
coVerifyOrder {
permissionStorage.updateSitePermissions(testPermissions)
- reload(browserSession)
+ reload(tab.id)
+ }
+ }
+
+ @Test
+ fun `handleAutoplayAdd should store the updated permission and reload webpage`() = coroutinesScope.runBlockingTest {
+ val testPermissions = mockk()
+
+ controller.handleAutoplayAdd(testPermissions)
+ advanceUntilIdle()
+
+ coVerifyOrder {
+ permissionStorage.add(testPermissions)
+ reload(tab.id)
}
}
}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt
new file mode 100644
index 000000000..8acfc08e8
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentReducerTest.kt
@@ -0,0 +1,75 @@
+/* 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.settings.quicksettings
+
+import mozilla.components.support.test.mock
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.settings.PhoneFeature
+
+@RunWith(FenixRobolectricTestRunner::class)
+class QuickSettingsFragmentReducerTest {
+
+ @Test
+ fun `WebsitePermissionAction - TogglePermission`() {
+ val toggleablePermission = WebsitePermission.Toggleable(
+ phoneFeature = PhoneFeature.CAMERA,
+ status = "status",
+ isVisible = false,
+ isEnabled = false,
+ isBlockedByAndroid = false
+ )
+
+ val map =
+ mapOf(PhoneFeature.CAMERA to toggleablePermission)
+
+ val state = QuickSettingsFragmentState(mock(), map)
+ val newState = quickSettingsFragmentReducer(
+ state,
+ WebsitePermissionAction.TogglePermission(
+ updatedFeature = PhoneFeature.CAMERA,
+ updatedStatus = "newStatus",
+ updatedEnabledStatus = true
+ )
+ )
+ val result = newState.websitePermissionsState[PhoneFeature.CAMERA]!!
+ assertEquals("newStatus", result.status)
+ assertTrue(result.isEnabled)
+ }
+
+ @Test
+ fun `WebsitePermissionAction - ChangeAutoplay`() {
+ val permissionPermission = WebsitePermission.Autoplay(
+ autoplayValue = AutoplayValue.BlockAll(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ ),
+ options = emptyList(),
+ isVisible = false
+ )
+
+ val map =
+ mapOf(PhoneFeature.AUTOPLAY to permissionPermission)
+
+ val state = QuickSettingsFragmentState(mock(), map)
+ val autoplayValue = AutoplayValue.AllowAll(
+ label = "newLabel",
+ rules = mock(),
+ sitePermission = null
+ )
+ val newState = quickSettingsFragmentReducer(
+ state,
+ WebsitePermissionAction.ChangeAutoplay(autoplayValue)
+ )
+
+ val result =
+ newState.websitePermissionsState[PhoneFeature.AUTOPLAY] as WebsitePermission.Autoplay
+ assertEquals(autoplayValue, result.autoplayValue)
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt
index 9b47d3a44..0a790ce04 100644
--- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsFragmentStoreTest.kt
@@ -6,12 +6,18 @@ package org.mozilla.fenix.settings.quicksettings
import android.content.pm.PackageManager
import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.runBlocking
+import mozilla.components.browser.state.state.content.PermissionHighlightsState
import mozilla.components.feature.sitepermissions.SitePermissions
+import mozilla.components.feature.sitepermissions.SitePermissionsRules
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.AutoplayAction
+import mozilla.components.feature.sitepermissions.SitePermissionsRules.Action
import mozilla.components.support.test.robolectric.testContext
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -19,6 +25,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
@@ -33,16 +40,34 @@ import org.mozilla.fenix.utils.Settings
@RunWith(FenixRobolectricTestRunner::class)
class QuickSettingsFragmentStoreTest {
private val context = spyk(testContext)
- private val permissions = mockk()
- private val appSettings = mockk()
+
+ @MockK(relaxed = true)
+ private lateinit var permissions: SitePermissions
+
+ @MockK(relaxed = true)
+ private lateinit var permissionHighlights: PermissionHighlightsState
+
+ @MockK(relaxed = true)
+ private lateinit var appSettings: Settings
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+
+ every { appSettings.getSitePermissionsCustomSettingsRules() } returns getRules()
+ }
@Test
fun `createStore constructs a QuickSettingsFragmentState`() {
- val settings = mockk(relaxed = true)
- val permissions = mockk(relaxed = true)
-
val store = QuickSettingsFragmentStore.createStore(
- context, "url", "Hello", "issuer", true, permissions, settings
+ context = context,
+ websiteUrl = "url",
+ websiteTitle = "Hello",
+ certificateName = "issuer",
+ isSecured = true,
+ permissions = permissions,
+ permissionHighlights = permissionHighlights,
+ settings = appSettings
)
assertNotNull(store)
@@ -83,6 +108,8 @@ class QuickSettingsFragmentStoreTest {
@Test
fun `createWebsitePermissionState helps in constructing an initial WebsitePermissionState for it's Store`() {
+ val permissionHighlights = mockk(relaxed = true)
+
every {
context.checkPermission(
any(),
@@ -96,12 +123,12 @@ class QuickSettingsFragmentStoreTest {
every { permissions.location } returns SitePermissions.Status.ALLOWED
every { permissions.localStorage } returns SitePermissions.Status.ALLOWED
every { permissions.mediaKeySystemAccess } returns SitePermissions.Status.NO_DECISION
- every { permissions.autoplayAudible } returns SitePermissions.Status.BLOCKED
- every { permissions.autoplayInaudible } returns SitePermissions.Status.BLOCKED
+ every { permissions.autoplayAudible } returns SitePermissions.AutoplayStatus.ALLOWED
+ every { permissions.autoplayInaudible } returns SitePermissions.AutoplayStatus.BLOCKED
every { appSettings.getAutoplayUserSetting(any()) } returns AUTOPLAY_BLOCK_ALL
val state = QuickSettingsFragmentStore.createWebsitePermissionState(
- context, permissions, appSettings
+ context, permissions, permissionHighlights, appSettings
)
// Just need to know that the WebsitePermissionsState properties are initialized.
@@ -115,6 +142,7 @@ class QuickSettingsFragmentStoreTest {
assertNotNull(state[PhoneFeature.AUTOPLAY_INAUDIBLE])
assertNotNull(state[PhoneFeature.PERSISTENT_STORAGE])
assertNotNull(state[PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS])
+ assertNotNull(state[PhoneFeature.AUTOPLAY])
}
@Test
@@ -129,8 +157,14 @@ class QuickSettingsFragmentStoreTest {
)
}.returns(PackageManager.PERMISSION_GRANTED)
every { permissions.camera } returns SitePermissions.Status.ALLOWED
+ every { permissionHighlights.isAutoPlayBlocking } returns true
- val websitePermission = cameraFeature.toWebsitePermission(context, permissions, appSettings)
+ val websitePermission = cameraFeature.toWebsitePermission(
+ context = context,
+ permissions = permissions,
+ permissionHighlights = permissionHighlights,
+ settings = appSettings
+ )
assertNotNull(websitePermission)
assertEquals(cameraFeature, websitePermission.phoneFeature)
@@ -138,14 +172,33 @@ class QuickSettingsFragmentStoreTest {
assertTrue(websitePermission.isVisible)
assertTrue(websitePermission.isEnabled)
assertFalse(websitePermission.isBlockedByAndroid)
+
+ val autoplayPermission = PhoneFeature.AUTOPLAY.toWebsitePermission(
+ context = context,
+ permissions = permissions,
+ permissionHighlights = permissionHighlights,
+ settings = appSettings
+ ) as WebsitePermission.Autoplay
+
+ assertNotNull(autoplayPermission)
+ assertNotNull(autoplayPermission.autoplayValue)
+ assertEquals(PhoneFeature.AUTOPLAY, autoplayPermission.phoneFeature)
+ assertTrue(websitePermission.isVisible)
+ assertTrue(websitePermission.isEnabled)
}
@Test
fun `PhoneFeature#getPermissionStatus gets the permission properties from delegates`() {
+ val permissionHighlights = mockk(relaxed = true)
val phoneFeature = PhoneFeature.CAMERA
every { permissions.camera } returns SitePermissions.Status.NO_DECISION
- val permissionsStatus = phoneFeature.toWebsitePermission(context, permissions, appSettings)
+ val permissionsStatus = phoneFeature.toWebsitePermission(
+ context,
+ permissions,
+ permissionHighlights,
+ appSettings
+ )
verify {
// Verifying phoneFeature.getActionLabel gets "Status(child of #2#4).ordinal()) was not called"
@@ -177,7 +230,7 @@ class QuickSettingsFragmentStoreTest {
val defaultEnabledStatus = true
val defaultBlockedByAndroidStatus = true
val websiteInfoState = mockk()
- val baseWebsitePermission = WebsitePermission(
+ val baseWebsitePermission = WebsitePermission.Toggleable(
phoneFeature = PhoneFeature.CAMERA,
status = "",
isVisible = true,
@@ -259,4 +312,15 @@ class QuickSettingsFragmentStoreTest {
assertEquals(defaultEnabledStatus, store.state.websitePermissionsState.getValue(PhoneFeature.LOCATION).isEnabled)
assertEquals(defaultBlockedByAndroidStatus, store.state.websitePermissionsState.getValue(PhoneFeature.LOCATION).isBlockedByAndroid)
}
+
+ private fun getRules() = SitePermissionsRules(
+ camera = Action.ASK_TO_ALLOW,
+ location = Action.ASK_TO_ALLOW,
+ microphone = Action.ASK_TO_ALLOW,
+ notification = Action.ASK_TO_ALLOW,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ persistentStorage = Action.ASK_TO_ALLOW,
+ mediaKeySystemAccess = Action.ASK_TO_ALLOW
+ )
}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt
index 556dd513f..09e5b349e 100644
--- a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/QuickSettingsInteractorTest.kt
@@ -38,4 +38,19 @@ class QuickSettingsInteractorTest {
assertTrue(permission.isCaptured)
assertEquals(websitePermission, permission.captured)
}
+
+ @Test
+ fun `onAutoplayChanged should delegate the controller`() {
+ val websitePermission = mockk()
+ val permission = slot()
+
+ interactor.onAutoplayChanged(websitePermission)
+
+ verify {
+ controller.handleAutoplayChanged(capture(permission))
+ }
+
+ assertTrue(permission.isCaptured)
+ assertEquals(websitePermission, permission.captured)
+ }
}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionViewTest.kt b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionViewTest.kt
new file mode 100644
index 000000000..96783eff8
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/settings/quicksettings/WebsitePermissionViewTest.kt
@@ -0,0 +1,212 @@
+/* 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.settings.quicksettings
+
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.appcompat.widget.AppCompatSpinner
+import androidx.core.view.isVisible
+import io.mockk.every
+import io.mockk.spyk
+import io.mockk.MockKAnnotations
+import io.mockk.verify
+import io.mockk.impl.annotations.MockK
+import junit.framework.TestCase.assertFalse
+import junit.framework.TestCase.assertTrue
+import junit.framework.TestCase.assertEquals
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.settings.PhoneFeature
+import org.mozilla.fenix.settings.quicksettings.WebsitePermissionsView.PermissionViewHolder.SpinnerPermission
+import org.mozilla.fenix.settings.quicksettings.WebsitePermissionsView.PermissionViewHolder.ToggleablePermission
+import java.util.EnumMap
+
+@RunWith(FenixRobolectricTestRunner::class)
+class WebsitePermissionViewTest {
+
+ @MockK(relaxed = true)
+ private lateinit var interactor: WebsitePermissionInteractor
+ private lateinit var view: WebsitePermissionsView
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ view = spyk(WebsitePermissionsView(FrameLayout(testContext), interactor))
+ }
+
+ @Test
+ fun `update - with visible permissions`() {
+ val label = TextView(testContext)
+ val status = TextView(testContext)
+ val permission = WebsitePermission.Toggleable(
+ phoneFeature = PhoneFeature.CAMERA,
+ status = "status",
+ isVisible = true,
+ isEnabled = true,
+ isBlockedByAndroid = false
+ )
+
+ val map = mapOf(PhoneFeature.CAMERA to permission)
+
+ view.permissionViews = EnumMap(
+ mapOf(PhoneFeature.CAMERA to ToggleablePermission(label, status))
+ )
+
+ every { view.bindPermission(any(), any()) } returns Unit
+
+ view.update(map)
+
+ verify { interactor.onPermissionsShown() }
+ verify { view.bindPermission(any(), any()) }
+ }
+
+ @Test
+ fun `update - with none visible permissions`() {
+ val label = TextView(testContext)
+ val status = TextView(testContext)
+ val permission = WebsitePermission.Toggleable(
+ phoneFeature = PhoneFeature.CAMERA,
+ status = "status",
+ isVisible = false,
+ isEnabled = true,
+ isBlockedByAndroid = false
+ )
+
+ val map = mapOf(PhoneFeature.CAMERA to permission)
+
+ view.permissionViews =
+ EnumMap(mapOf(PhoneFeature.CAMERA to ToggleablePermission(label, status)))
+
+ every { view.bindPermission(any(), any()) } returns Unit
+
+ view.update(map)
+
+ verify(exactly = 0) { interactor.onPermissionsShown() }
+ verify { view.bindPermission(any(), any()) }
+ }
+
+ @Test
+ fun `bindPermission - a visible ToggleablePermission`() {
+ val label = TextView(testContext)
+ val status = TextView(testContext)
+ val permissionView = ToggleablePermission(label, status)
+ val permission = WebsitePermission.Toggleable(
+ phoneFeature = PhoneFeature.CAMERA,
+ status = "status",
+ isVisible = true,
+ isEnabled = true,
+ isBlockedByAndroid = false
+ )
+
+ view.permissionViews = EnumMap(mapOf(PhoneFeature.CAMERA to permissionView))
+
+ every { interactor.onPermissionToggled(any()) } returns Unit
+
+ view.bindPermission(permission, permissionView)
+
+ assertTrue(permissionView.label.isVisible)
+ assertTrue(permissionView.label.isEnabled)
+ assertTrue(permissionView.status.isVisible)
+ assertEquals(permission.status, permissionView.status.text)
+
+ permissionView.status.performClick()
+
+ verify { interactor.onPermissionToggled(any()) }
+ }
+
+ @Test
+ fun `bindPermission - a not visible ToggleablePermission`() {
+ val label = TextView(testContext)
+ val status = TextView(testContext)
+ val permissionView = ToggleablePermission(label, status)
+ val permission = WebsitePermission.Toggleable(
+ phoneFeature = PhoneFeature.CAMERA,
+ status = "status",
+ isVisible = false,
+ isEnabled = false,
+ isBlockedByAndroid = false
+ )
+
+ view.permissionViews = EnumMap(mapOf(PhoneFeature.CAMERA to permissionView))
+
+ every { interactor.onPermissionToggled(any()) } returns Unit
+
+ view.bindPermission(permission, permissionView)
+
+ assertFalse(permissionView.label.isVisible)
+ assertFalse(permissionView.label.isEnabled)
+ assertFalse(permissionView.status.isVisible)
+ assertEquals(permission.status, permissionView.status.text)
+
+ permissionView.status.performClick()
+
+ verify { interactor.onPermissionToggled(any()) }
+ }
+
+ @Test
+ fun `bindPermission - a visible SpinnerPermission`() {
+ val label = TextView(testContext)
+ val status = AppCompatSpinner(testContext)
+ val permissionView = SpinnerPermission(label, status)
+ val options = listOf(
+ AutoplayValue.BlockAll(
+ label = "BlockAll",
+ rules = mock(),
+ sitePermission = null
+ ),
+ AutoplayValue.AllowAll(
+ label = "AllowAll",
+ rules = mock(),
+ sitePermission = null
+ ),
+ AutoplayValue.BlockAudible(
+ label = "BlockAudible",
+ rules = mock(),
+ sitePermission = null
+ )
+ )
+ val permission = WebsitePermission.Autoplay(
+ autoplayValue = options[0],
+ options = options,
+ isVisible = true
+ )
+
+ view.permissionViews = EnumMap(mapOf(PhoneFeature.AUTOPLAY to permissionView))
+
+ every { interactor.onAutoplayChanged(any()) } returns Unit
+
+ view.bindPermission(permission, permissionView)
+
+ assertTrue(permissionView.label.isVisible)
+ assertFalse(permissionView.label.isEnabled)
+ assertTrue(permissionView.status.isVisible)
+ assertEquals(permission.autoplayValue, permissionView.status.selectedItem)
+
+ permissionView.status.onItemSelectedListener!!.onItemSelected(
+ mock(),
+ permissionView.status,
+ 1,
+ 0L
+ )
+
+ // Selecting the same item should not trigger a selection event.
+ verify(exactly = 0) { interactor.onAutoplayChanged(permissionView.status.selectedItem as AutoplayValue) }
+
+ permissionView.status.setSelection(2)
+ permissionView.status.onItemSelectedListener!!.onItemSelected(
+ mock(),
+ permissionView.status,
+ 2,
+ 0L
+ )
+
+ // Selecting a different item from the selected one should trigger an selection event.
+ verify(exactly = 1) { interactor.onAutoplayChanged(permissionView.status.selectedItem as AutoplayValue) }
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragmentTest.kt
new file mode 100644
index 000000000..262cd08d9
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsDetailsExceptionsFragmentTest.kt
@@ -0,0 +1,106 @@
+/* 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.settings.sitepermissions
+
+import androidx.preference.Preference
+import io.mockk.MockKAnnotations
+import io.mockk.every
+import io.mockk.impl.annotations.MockK
+import io.mockk.spyk
+import io.mockk.verify
+import mozilla.components.feature.sitepermissions.SitePermissions
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.settings.PhoneFeature
+import org.mozilla.fenix.utils.Settings
+
+@RunWith(FenixRobolectricTestRunner::class)
+class SitePermissionsDetailsExceptionsFragmentTest {
+ @MockK(relaxed = true)
+ private lateinit var settings: Settings
+
+ @MockK(relaxed = true)
+ private lateinit var permissions: SitePermissions
+
+ private lateinit var fragment: SitePermissionsDetailsExceptionsFragment
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+ fragment = spyk(SitePermissionsDetailsExceptionsFragment())
+
+ fragment.sitePermissions = permissions
+
+ every { permissions.origin } returns "mozilla.org"
+ every { fragment.provideContext() } returns testContext
+ every { fragment.provideSettings() } returns settings
+ }
+
+ @Test
+ fun `WHEN bindCategoryPhoneFeatures is called THEN all categories must be initialized`() {
+
+ every { fragment.initPhoneFeature(any()) } returns Unit
+ every { fragment.initAutoplayFeature() } returns Unit
+ every { fragment.bindClearPermissionsButton() } returns Unit
+
+ fragment.bindCategoryPhoneFeatures()
+
+ verify {
+ fragment.initPhoneFeature(PhoneFeature.CAMERA)
+ fragment.initPhoneFeature(PhoneFeature.LOCATION)
+ fragment.initPhoneFeature(PhoneFeature.MICROPHONE)
+ fragment.initPhoneFeature(PhoneFeature.NOTIFICATION)
+ fragment.initPhoneFeature(PhoneFeature.PERSISTENT_STORAGE)
+ fragment.initPhoneFeature(PhoneFeature.MEDIA_KEY_SYSTEM_ACCESS)
+ fragment.initAutoplayFeature()
+ fragment.bindClearPermissionsButton()
+ }
+ }
+
+ @Test
+ fun `WHEN initPhoneFeature is called THEN the feature label must be bind and a click listener must be attached`() {
+ val feature = spyk(PhoneFeature.CAMERA)
+ val label = "label"
+ val preference = spyk(Preference(testContext))
+
+ every { feature.getActionLabel(any(), any()) } returns label
+ every { fragment.getPreference((any())) } returns preference
+ every { fragment.navigateToPhoneFeature((any())) } returns Unit
+
+ fragment.initPhoneFeature(feature)
+
+ assertEquals(label, preference.summary)
+
+ preference.performClick()
+
+ verify {
+ fragment.navigateToPhoneFeature(feature)
+ }
+ }
+
+ @Test
+ fun `WHEN initAutoplayFeature THEN the autoplay label must be bind and a click listener must be attached`() {
+ val label = "label"
+ val preference = spyk(Preference(testContext))
+
+ every { fragment.getAutoplayLabel() } returns label
+ every { fragment.getPreference((any())) } returns preference
+ every { fragment.navigateToPhoneFeature((any())) } returns Unit
+
+ fragment.initAutoplayFeature()
+
+ assertEquals(label, preference.summary)
+
+ preference.performClick()
+
+ verify {
+ fragment.navigateToPhoneFeature(PhoneFeature.AUTOPLAY)
+ }
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragmentTest.kt
new file mode 100644
index 000000000..76450ce18
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/settings/sitepermissions/SitePermissionsManageExceptionsPhoneFeatureFragmentTest.kt
@@ -0,0 +1,538 @@
+/* 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.settings.sitepermissions
+
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.RadioButton
+import androidx.core.view.isVisible
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import io.mockk.MockKAnnotations
+import io.mockk.impl.annotations.MockK
+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.feature.sitepermissions.SitePermissionsRules.Action
+import mozilla.components.support.test.mock
+import mozilla.components.support.test.robolectric.testContext
+import org.junit.Assert.assertTrue
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.R
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+import org.mozilla.fenix.settings.PhoneFeature
+import org.mozilla.fenix.settings.quicksettings.AutoplayValue
+import org.mozilla.fenix.utils.Settings
+
+@RunWith(FenixRobolectricTestRunner::class)
+class SitePermissionsManageExceptionsPhoneFeatureFragmentTest {
+ @MockK(relaxed = true)
+ private lateinit var settings: Settings
+
+ @MockK(relaxed = true)
+ private lateinit var permissions: SitePermissions
+
+ private lateinit var fragment: SitePermissionsManageExceptionsPhoneFeatureFragment
+
+ @Before
+ fun setup() {
+ MockKAnnotations.init(this)
+
+ fragment = spyk(SitePermissionsManageExceptionsPhoneFeatureFragment())
+ fragment.rootView = mockk(relaxed = true)
+
+ every { fragment.requireContext() } returns testContext
+ every { fragment.getSettings() } returns settings
+ }
+
+ @Test
+ fun `GIVEN an AUTOPLAY permission WHEN onCreateView is called THEN initAutoplay is called`() {
+ every { fragment.getFeature() } returns PhoneFeature.AUTOPLAY
+ every { fragment.initAutoplay(permissions) } returns Unit
+ every { fragment.getSitePermission() } returns permissions
+
+ fragment.onCreateView(LayoutInflater.from(testContext), null, null)
+
+ verify {
+ fragment.initAutoplay(permissions)
+ fragment.bindBlockedByAndroidContainer()
+ fragment.initClearPermissionsButton()
+ }
+ }
+
+ @Test
+ fun `GIVEN a none AUTOPLAY permission WHEN onCreateView is called THEN initNormalFeature is called`() {
+ val features = PhoneFeature.values().filter { it != PhoneFeature.AUTOPLAY }
+
+ features.forEach {
+ every { fragment.getFeature() } returns it
+ every { fragment.initNormalFeature() } returns Unit
+ every { fragment.getSitePermission() } returns permissions
+
+ fragment.onCreateView(LayoutInflater.from(testContext), null, null)
+
+ verify {
+ fragment.initNormalFeature()
+ fragment.bindBlockedByAndroidContainer()
+ fragment.initClearPermissionsButton()
+ }
+ }
+ }
+
+ @Test
+ fun `WHEN initAutoplay is called THEN AllowAll, BlockAll and BlockAudible radio options will be configure`() {
+ every { fragment.initAutoplayOption(any(), any()) } returns Unit
+ every { fragment.getSitePermission() } returns permissions
+ every { settings.getSitePermissionsCustomSettingsRules() } returns getRules()
+
+ fragment.initAutoplay()
+
+ verify {
+ fragment.initAutoplayOption(R.id.ask_to_allow_radio, any())
+ fragment.initAutoplayOption(R.id.block_radio, any())
+ fragment.initAutoplayOption(R.id.optional_radio, any())
+ }
+ }
+
+ @Test
+ fun `WHEN initAutoplayOption is called THEN the radio button will visible and a click listener will be attached`() {
+ val radioButton = spyk(RadioButton(testContext))
+ val rootView = mockk()
+ val autoplayValue = mockk(relaxed = true)
+
+ radioButton.isVisible = false
+
+ fragment.rootView = rootView
+ every { rootView.findViewById(any()) } returns radioButton
+ every { autoplayValue.label } returns "label"
+ with(fragment) {
+ every { updatedSitePermissions(any()) } returns Unit
+ every { any().restoreState(any()) } returns Unit
+ }
+
+ fragment.initAutoplayOption(R.id.ask_to_allow_radio, autoplayValue)
+
+ assertTrue(radioButton.isVisible)
+ assertEquals(autoplayValue.label, radioButton.text)
+
+ with(fragment) {
+ verify {
+ any().restoreState(autoplayValue)
+ }
+ }
+ }
+
+ @Test
+ fun `GIVEN a AllowAll value with autoplayAudible and autoplayInaudible rules are ALLOWED WHEN isSelected is called THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a AllowAll value with autoplayAudible ALLOWED and autoplayInaudible BLOCKED rules WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a AllowAll value with sitePermission autoplayAudible and autoplayInaudible are ALLOWED WHEN isSelected is called THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a AllowAll value with sitePermission autoplayAudible and autoplayInaudible are BLOCKED WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.ALLOWED,
+ autoplayAudible = AutoplayAction.BLOCKED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAll value with autoplayAudible and autoplayInaudible rules are BLOCKED WHEN isSelected is called THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAll value with autoplayInaudible BLOCKED and autoplayAudible ALLOWED rules WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAll value with sitePermission autoplayAudible and autoplayInaudible are BLOCKED WHEN isSelected THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAll value with sitePermission autoplayAudible ALLOWED and autoplayInaudible BLOCKED WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible value with autoplayAudible BLOCKED and autoplayInaudible ALLOWED rules WHEN isSelected is called THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible value with autoplayInaudible and autoplayAudible BLOCKED rules WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.BLOCKED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible with sitePermission autoplayAudible BLOCKED and autoplayInaudible ALLOWED WHEN isSelected is called THEN isSelected will be true`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+ )
+
+ assertTrue(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible with sitePermission autoplayAudible ALLOWED and autoplayInaudible BLOCKED WHEN isSelected is called THEN isSelected will be false`() {
+ val rules = getRules().copy(
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ autoplayAudible = AutoplayAction.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = SitePermissions(
+ origin = "",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+ )
+
+ assertFalse(value.isSelected())
+ }
+
+ @Test
+ fun `GIVEN a AllowAll WHEN createSitePermissionsFromCustomRules is called THEN rules will included autoplayAudible and autoplayInaudible ALLOWED`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `GIVEN a BlockAll WHEN createSitePermissionsFromCustomRules is called THEN rules will included autoplayAudible and autoplayInaudible BLOCKED`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible WHEN createSitePermissionsFromCustomRules is called THEN rules will included autoplayAudible BLOCKED and autoplayInaudible ALLOWED`() {
+ val rules = getRules().copy(
+ autoplayAudible = AutoplayAction.ALLOWED,
+ autoplayInaudible = AutoplayAction.ALLOWED
+ )
+
+ every { settings.getSitePermissionsCustomSettingsRules() } returns rules
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = rules,
+ sitePermission = null
+ )
+
+ val result = value.createSitePermissionsFromCustomRules("mozilla.org", settings)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(rules.camera.toStatus(), result.camera)
+ assertEquals(rules.location.toStatus(), result.location)
+ assertEquals(rules.microphone.toStatus(), result.microphone)
+ assertEquals(rules.notification.toStatus(), result.notification)
+ assertEquals(rules.persistentStorage.toStatus(), result.localStorage)
+ assertEquals(rules.mediaKeySystemAccess.toStatus(), result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `GIVEN a AllowAll WHEN updateSitePermissions is called THEN site permissions will include autoplayAudible and autoplayInaudible ALLOWED`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.BLOCKED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+
+ val value = AutoplayValue.AllowAll(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `GIVEN a BlockAll WHEN updateSitePermissions is called THEN site permissions will include autoplayAudible and autoplayInaudible BLOCKED`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.ALLOWED
+ )
+
+ val value = AutoplayValue.BlockAll(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `GIVEN a BlockAudible WHEN updateSitePermissions is called THEN site permissions will include autoplayAudible BLOCKED and autoplayInaudible ALLOWED`() {
+ val sitePermissions = SitePermissions(
+ origin = "origin",
+ savedAt = 0L,
+ autoplayAudible = AutoplayStatus.ALLOWED,
+ autoplayInaudible = AutoplayStatus.BLOCKED
+ )
+
+ val value = AutoplayValue.BlockAudible(
+ label = "label",
+ rules = mock(),
+ sitePermission = null
+ )
+
+ val result = value.updateSitePermissions(sitePermissions)
+
+ assertEquals(AutoplayStatus.BLOCKED, result.autoplayAudible)
+ assertEquals(AutoplayStatus.ALLOWED, result.autoplayInaudible)
+ assertEquals(sitePermissions.camera, result.camera)
+ assertEquals(sitePermissions.location, result.location)
+ assertEquals(sitePermissions.microphone, result.microphone)
+ assertEquals(sitePermissions.notification, result.notification)
+ assertEquals(sitePermissions.localStorage, result.localStorage)
+ assertEquals(sitePermissions.mediaKeySystemAccess, result.mediaKeySystemAccess)
+ }
+
+ @Test
+ fun `WHEN calling AutoplayValue values THEN values for AllowAll,BlockAll and BlockAudible will be returned`() {
+ val values = AutoplayValue.values(testContext, settings, null)
+
+ assertTrue(values.any { it is AutoplayValue.AllowAll })
+ assertTrue(values.any { it is AutoplayValue.BlockAll })
+ assertTrue(values.any { it is AutoplayValue.BlockAudible })
+ assertEquals(3, values.size)
+ }
+
+ private fun getRules() = SitePermissionsRules(
+ camera = Action.ASK_TO_ALLOW,
+ location = Action.ASK_TO_ALLOW,
+ microphone = Action.ASK_TO_ALLOW,
+ notification = Action.ASK_TO_ALLOW,
+ autoplayAudible = AutoplayAction.BLOCKED,
+ autoplayInaudible = AutoplayAction.BLOCKED,
+ persistentStorage = Action.ASK_TO_ALLOW,
+ mediaKeySystemAccess = Action.ASK_TO_ALLOW
+ )
+}
diff --git a/app/src/test/java/org/mozilla/fenix/shortcut/PwaOnboardingObserverTest.kt b/app/src/test/java/org/mozilla/fenix/shortcut/PwaOnboardingObserverTest.kt
index 9ed456972..84f3ca5d7 100644
--- a/app/src/test/java/org/mozilla/fenix/shortcut/PwaOnboardingObserverTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/shortcut/PwaOnboardingObserverTest.kt
@@ -10,6 +10,7 @@ import androidx.lifecycle.LifecycleRegistry
import androidx.navigation.NavController
import io.mockk.every
import io.mockk.mockk
+import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineDispatcher
@@ -20,12 +21,10 @@ import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.pwa.WebAppUseCases
import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import org.mozilla.fenix.R
-import org.mozilla.fenix.browser.BrowserFragmentDirections
-import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.utils.Settings
@ExperimentalCoroutinesApi
@@ -53,57 +52,60 @@ class PwaOnboardingObserverTest {
)
)
lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
navigationController = mockk(relaxed = true)
settings = mockk(relaxed = true)
webAppUseCases = mockk(relaxed = true)
- pwaOnboardingObserver = PwaOnboardingObserver(
+ pwaOnboardingObserver = spyk(PwaOnboardingObserver(
store = store,
lifecycleOwner = lifecycleOwner,
navController = navigationController,
settings = settings,
webAppUseCases = webAppUseCases
- )
+ ))
+ every { pwaOnboardingObserver.navigateToPwaOnboarding() } returns Unit
+ }
+
+ @After
+ fun teardown() {
+ pwaOnboardingObserver.stop()
}
@Test
fun `GIVEN cfr should not yet be shown WHEN installable page is loaded THEN counter is incremented`() {
- pwaOnboardingObserver.start()
every { webAppUseCases.isInstallable() } returns true
+ every { settings.userKnowsAboutPwas } returns false
+ every { settings.shouldShowPwaCfr } returns false
+ pwaOnboardingObserver.start()
store.dispatch(ContentAction.UpdateWebAppManifestAction("1", mockk())).joinBlocking()
verify { settings.incrementVisitedInstallableCount() }
- verify(exactly = 0) { navigationController.nav(
- R.id.browserFragment,
- BrowserFragmentDirections.actionBrowserFragmentToPwaOnboardingDialogFragment())
- }
+ verify(exactly = 0) { pwaOnboardingObserver.navigateToPwaOnboarding() }
}
@Test
fun `GIVEN cfr should be shown WHEN installable page is loaded THEN we navigate to onboarding fragment`() {
- pwaOnboardingObserver.start()
every { webAppUseCases.isInstallable() } returns true
+ every { settings.userKnowsAboutPwas } returns false
every { settings.shouldShowPwaCfr } returns true
+ pwaOnboardingObserver.start()
store.dispatch(ContentAction.UpdateWebAppManifestAction("1", mockk())).joinBlocking()
verify { settings.incrementVisitedInstallableCount() }
- verify { navigationController.nav(
- R.id.browserFragment,
- BrowserFragmentDirections.actionBrowserFragmentToPwaOnboardingDialogFragment())
- }
+ verify { pwaOnboardingObserver.navigateToPwaOnboarding() }
}
@Test
fun `GIVEN web app is not installable WHEN page with manifest is loaded THEN nothing happens`() {
- pwaOnboardingObserver.start()
every { webAppUseCases.isInstallable() } returns false
+ every { settings.userKnowsAboutPwas } returns false
+ every { settings.shouldShowPwaCfr } returns true
+ pwaOnboardingObserver.start()
store.dispatch(ContentAction.UpdateWebAppManifestAction("1", mockk())).joinBlocking()
verify(exactly = 0) { settings.incrementVisitedInstallableCount() }
- verify(exactly = 0) { navigationController.nav(
- R.id.browserFragment,
- BrowserFragmentDirections.actionBrowserFragmentToPwaOnboardingDialogFragment())
- }
+ verify(exactly = 0) { pwaOnboardingObserver.navigateToPwaOnboarding() }
}
internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
diff --git a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt
index 41c5d3e73..dc79446f8 100644
--- a/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/tabhistory/TabHistoryControllerTest.kt
@@ -5,62 +5,53 @@
package org.mozilla.fenix.tabhistory
import androidx.navigation.NavController
-import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
-import mozilla.components.browser.session.Session
-import mozilla.components.browser.session.SessionManager
-import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.feature.session.SessionUseCases
+import org.junit.Before
import org.junit.Test
class TabHistoryControllerTest {
- private val store: BrowserStore = mockk(relaxed = true)
- private val sessionManager: SessionManager = mockk(relaxed = true)
- private val navController: NavController = mockk(relaxed = true)
- private val sessionUseCases = SessionUseCases(store, sessionManager)
-
- private val goToHistoryIndexUseCase = sessionUseCases.goToHistoryIndex
- private val controller = DefaultTabHistoryController(
- navController = navController,
- goToHistoryIndexUseCase = goToHistoryIndexUseCase,
- sessionManager = sessionManager
- )
-
- private val currentItem = TabHistoryItem(
- index = 0,
- title = "",
- url = "",
- isSelected = true
- )
+ private lateinit var navController: NavController
+ private lateinit var goToHistoryIndexUseCase: SessionUseCases.GoToHistoryIndexUseCase
+ private lateinit var currentItem: TabHistoryItem
+
+ @Before
+ fun setUp() {
+ navController = mockk(relaxed = true)
+ goToHistoryIndexUseCase = mockk(relaxed = true)
+ currentItem = TabHistoryItem(
+ index = 0,
+ title = "",
+ url = "",
+ isSelected = true
+ )
+ }
@Test
fun handleGoToHistoryIndexNormalBrowsing() {
- val session: Session = mockk(relaxed = true)
- every { sessionManager.selectedSession } returns session
+ val controller = DefaultTabHistoryController(
+ navController = navController,
+ goToHistoryIndexUseCase = goToHistoryIndexUseCase
+ )
controller.handleGoToHistoryItem(currentItem)
-
verify { navController.navigateUp() }
- verify { goToHistoryIndexUseCase.invoke(currentItem.index, session) }
+ verify { goToHistoryIndexUseCase.invoke(currentItem.index) }
}
@Test
fun handleGoToHistoryIndexCustomTab() {
+ val customTabId = "customTabId"
val customTabController = DefaultTabHistoryController(
navController = navController,
goToHistoryIndexUseCase = goToHistoryIndexUseCase,
- customTabId = "custom-id",
- sessionManager = sessionManager
+ customTabId = customTabId
)
- val customTabSession: Session = mockk(relaxed = true)
-
- every { sessionManager.findSessionById(any()) } returns customTabSession
customTabController.handleGoToHistoryItem(currentItem)
-
verify { navController.navigateUp() }
- verify { goToHistoryIndexUseCase.invoke(currentItem.index, customTabSession) }
+ verify { goToHistoryIndexUseCase.invoke(currentItem.index, customTabId) }
}
}
diff --git a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt
index 284bb0de9..8e7f7599a 100644
--- a/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/tabtray/DefaultTabTrayControllerTest.kt
@@ -15,12 +15,12 @@ import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
+import io.mockk.verifyAll
import io.mockk.verifyOrder
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScope
-import mozilla.components.browser.session.Session
-import mozilla.components.browser.session.SessionManager
import mozilla.components.browser.state.state.BrowserState
+import mozilla.components.browser.state.state.TabSessionState
import mozilla.components.browser.state.state.createTab
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.concept.base.profiler.Profiler
@@ -40,28 +40,18 @@ import org.mozilla.fenix.browser.browsingmode.BrowsingModeManager
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.sessionsOfType
@OptIn(ExperimentalCoroutinesApi::class)
class DefaultTabTrayControllerTest {
private val activity: HomeActivity = mockk(relaxed = true)
private val profiler: Profiler? = mockk(relaxed = true)
private val navController: NavController = mockk()
- private val sessionManager: SessionManager = mockk(relaxed = true)
- var store = BrowserStore(
- BrowserState(
- tabs = listOf(
- createTab(url = "http://firefox.com", id = "5678"),
- createTab(url = "http://mozilla.org", id = "1234")
- ), selectedTabId = "1234"
- )
- )
private val browsingModeManager: BrowsingModeManager = mockk(relaxed = true)
private val dismissTabTray: (() -> Unit) = mockk(relaxed = true)
private val dismissTabTrayAndNavigateHome: ((String) -> Unit) = mockk(relaxed = true)
private val registerCollectionStorageObserver: (() -> Unit) = mockk(relaxed = true)
- private val showChooseCollectionDialog: ((List) -> Unit) = mockk(relaxed = true)
- private val showAddNewCollectionDialog: ((List) -> Unit) = mockk(relaxed = true)
+ private val showChooseCollectionDialog: ((List) -> Unit) = mockk(relaxed = true)
+ private val showAddNewCollectionDialog: ((List) -> Unit) = mockk(relaxed = true)
private val tabCollectionStorage: TabCollectionStorage = mockk(relaxed = true)
private val bookmarksStorage: BookmarksStorage = mockk(relaxed = true)
private val tabCollection: TabCollection = mockk()
@@ -76,29 +66,20 @@ class DefaultTabTrayControllerTest {
private lateinit var controller: DefaultTabTrayController
- private val session = Session(
- "mozilla.org",
- true
- )
-
- private val nonPrivateSession = Session(
- "mozilla.org",
- false
- )
+ private val tab1 = createTab(url = "http://firefox.com", id = "5678")
+ private val tab2 = createTab(url = "http://mozilla.org", id = "1234")
@Before
fun setUp() {
mockkStatic("org.mozilla.fenix.ext.SessionManagerKt")
- every { sessionManager.sessionsOfType(private = true) } returns listOf(session).asSequence()
- every { sessionManager.sessionsOfType(private = false) } returns listOf(nonPrivateSession).asSequence()
- every { sessionManager.createSessionSnapshot(any()) } returns SessionManager.Snapshot.Item(
- session
+ val store = BrowserStore(
+ BrowserState(
+ tabs = listOf(tab1, tab2), selectedTabId = tab2.id
+ )
)
- every { sessionManager.findSessionById("1234") } returns session
- every { sessionManager.remove(any()) } just Runs
+
every { tabCollectionStorage.cachedTabCollections } returns cachedTabCollections
- every { sessionManager.selectedSession } returns nonPrivateSession
every { navController.navigate(any()) } just Runs
every { navController.currentDestination } returns currentDestination
every { currentDestination.id } returns R.id.browserFragment
@@ -107,7 +88,6 @@ class DefaultTabTrayControllerTest {
controller = DefaultTabTrayController(
activity = activity,
profiler = profiler,
- sessionManager = sessionManager,
browserStore = store,
browsingModeManager = browsingModeManager,
tabCollectionStorage = tabCollectionStorage,
@@ -268,13 +248,14 @@ class DefaultTabTrayControllerTest {
@Test
fun onSaveToCollectionClicked() {
- val tab = Tab("1234", "mozilla.org")
+ val tab = Tab(tab2.id, tab2.content.url)
controller.handleSaveToCollectionClicked(setOf(tab))
- verify {
+
+ verifyAll {
metrics.track(Event.TabsTraySaveToCollectionPressed)
registerCollectionStorageObserver()
- showChooseCollectionDialog(listOf(session))
+ showChooseCollectionDialog(listOf(tab2))
}
}
@@ -299,7 +280,7 @@ class DefaultTabTrayControllerTest {
val tab = Tab("1234", "mozilla.org")
controller.handleDeleteSelectedTabs(setOf(tab))
- verify {
+ verifyAll {
tabsUseCases.removeTabs(listOf(tab.id))
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showUndoSnackbarForTabs()
@@ -312,19 +293,31 @@ class DefaultTabTrayControllerTest {
coEvery { bookmarksStorage.getBookmarksWithUrl("mozilla.org") } returns listOf()
controller.handleBookmarkSelectedTabs(setOf(tab))
- verify {
+ verifyAll {
tabTrayFragmentStore.dispatch(TabTrayDialogFragmentAction.ExitMultiSelectMode)
showBookmarksSavedSnackbar()
}
}
@Test
- fun handleSetUpAutoCloseTabsClicked() {
+ fun handleRecentlyClosedClicked() {
+ controller.handleRecentlyClosedClicked()
+ val directions = TabTrayDialogFragmentDirections.actionGlobalRecentlyClosed()
+
+ verifyAll {
+ navController.navigate(directions)
+ metrics.track(Event.RecentlyClosedTabsOpened)
+ }
+ }
+
+ @Test
+ fun handleGoToTabsSettingClicked() {
controller.handleGoToTabsSettingClicked()
val directions = TabTrayDialogFragmentDirections.actionGlobalTabSettingsFragment()
- verify {
+ verifyAll {
navController.navigate(directions)
+ metrics.track(Event.TabsTrayCfrTapped)
}
}
}
diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt
index 81050a3e4..59f5753c7 100644
--- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionOverlayTest.kt
@@ -6,15 +6,30 @@ package org.mozilla.fenix.trackingprotection
import android.content.Context
import android.view.View
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
import io.mockk.MockKAnnotations
import io.mockk.every
-import io.mockk.impl.annotations.MockK
-import io.mockk.mockk
import io.mockk.spyk
import io.mockk.verify
-import mozilla.components.browser.session.Session
+import io.mockk.mockk
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.SessionState
+import mozilla.components.browser.state.state.TrackingProtectionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.fenix.R
@@ -22,6 +37,7 @@ import org.mozilla.fenix.components.metrics.MetricController
import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
import org.mozilla.fenix.utils.Settings
+@ExperimentalCoroutinesApi
@RunWith(FenixRobolectricTestRunner::class)
class TrackingProtectionOverlayTest {
@@ -30,25 +46,76 @@ class TrackingProtectionOverlayTest {
@MockK(relaxed = true) private lateinit var metrics: MetricController
@MockK(relaxed = true) private lateinit var toolbar: View
@MockK(relaxed = true) private lateinit var icon: View
- @MockK(relaxed = true) private lateinit var session: Session
+ @MockK(relaxed = true) private lateinit var session: SessionState
@MockK(relaxed = true) private lateinit var overlay: TrackingProtectionOverlay
+ private val testDispatcher = TestCoroutineDispatcher()
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule(testDispatcher)
+ private lateinit var store: BrowserStore
+
@Before
fun setup() {
MockKAnnotations.init(this)
context = spyk(testContext)
-
- overlay = TrackingProtectionOverlay(context, settings, metrics) { toolbar }
+ store = BrowserStore()
+ val lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ overlay = spyk(
+ TrackingProtectionOverlay(
+ context,
+ settings,
+ metrics,
+ store,
+ lifecycleOwner
+ ) { toolbar })
every { toolbar.findViewById(R.id.mozac_browser_toolbar_tracking_protection_indicator) } returns icon
}
+ @After
+ fun cleanUp() {
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun `WHEN loading state changes THEN overlay is notified`() {
+ val tab = createTab("mozilla.org")
+ every { overlay.onLoadingStateChanged(tab) } returns Unit
+
+ overlay.start()
+
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+ store.dispatch(ContentAction.UpdateLoadingStateAction(tab.id, false)).joinBlocking()
+
+ val selectedTab = store.state.findTab(tab.id)!!
+
+ verify(exactly = 1) {
+ overlay.onLoadingStateChanged(selectedTab)
+ }
+ }
+
+ @Test
+ fun `WHEN overlay is stopped THEN listeners must be unsubscribed`() {
+ every { overlay.cancelScope() } returns Unit
+
+ overlay.stop()
+
+ verify(exactly = 1) {
+ overlay.cancelScope()
+ }
+ }
+
@Test
fun `no-op when loading`() {
+ val trackingProtection =
+ TrackingProtectionState(enabled = true, blockedTrackers = listOf(mockk()))
every { settings.shouldShowTrackingProtectionCfr } returns true
- every { session.trackerBlockingEnabled } returns true
- every { session.trackersBlocked } returns listOf(mockk())
+ every { session.trackingProtection } returns trackingProtection
+ every { session.content.loading } returns true
- overlay.onLoadingStateChanged(session, loading = true)
+ overlay.onLoadingStateChanged(session)
verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() }
}
@@ -56,26 +123,32 @@ class TrackingProtectionOverlayTest {
fun `no-op when should not show onboarding`() {
every { settings.shouldShowTrackingProtectionCfr } returns false
- overlay.onLoadingStateChanged(session, loading = false)
+ every { session.content.loading } returns false
+
+ overlay.onLoadingStateChanged(session)
verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() }
}
@Test
fun `no-op when tracking protection disabled`() {
every { settings.shouldShowTrackingProtectionCfr } returns true
- every { session.trackerBlockingEnabled } returns false
+ every { session.trackingProtection } returns TrackingProtectionState(enabled = false)
+ every { session.content.loading } returns false
- overlay.onLoadingStateChanged(session, loading = false)
+ overlay.onLoadingStateChanged(session)
verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() }
}
@Test
fun `no-op when no trackers blocked`() {
every { settings.shouldShowTrackingProtectionCfr } returns true
- every { session.trackerBlockingEnabled } returns true
- every { session.trackersBlocked } returns emptyList()
+ every { session.content.loading } returns false
+ every { session.trackingProtection } returns TrackingProtectionState(
+ enabled = true,
+ blockedTrackers = emptyList()
+ )
- overlay.onLoadingStateChanged(session, loading = false)
+ overlay.onLoadingStateChanged(session)
verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() }
}
@@ -83,10 +156,12 @@ class TrackingProtectionOverlayTest {
fun `show onboarding when trackers are blocked`() {
every { toolbar.hasWindowFocus() } returns true
every { settings.shouldShowTrackingProtectionCfr } returns true
- every { session.trackerBlockingEnabled } returns true
- every { session.trackersBlocked } returns listOf(mockk())
-
- overlay.onLoadingStateChanged(session, loading = false)
+ every { session.content.loading } returns false
+ every { session.trackingProtection } returns TrackingProtectionState(
+ enabled = true,
+ blockedTrackers = listOf(mockk())
+ )
+ overlay.onLoadingStateChanged(session)
verify { settings.incrementTrackingProtectionOnboardingCount() }
}
@@ -94,10 +169,22 @@ class TrackingProtectionOverlayTest {
fun `no-op when toolbar doesn't have focus`() {
every { toolbar.hasWindowFocus() } returns false
every { settings.shouldShowTrackingProtectionCfr } returns true
- every { session.trackerBlockingEnabled } returns true
- every { session.trackersBlocked } returns listOf(mockk())
-
- overlay.onLoadingStateChanged(session, loading = false)
+ every { session.content.loading } returns false
+ every { session.trackingProtection } returns TrackingProtectionState(
+ enabled = true,
+ blockedTrackers = listOf(mockk())
+ )
+ overlay.onLoadingStateChanged(session)
+
+ overlay.onLoadingStateChanged(session)
verify(exactly = 0) { settings.incrementTrackingProtectionOnboardingCount() }
}
+
+ internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ private val lifecycleRegistry = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+
+ override fun getLifecycle(): Lifecycle = lifecycleRegistry
+ }
}
diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt
new file mode 100644
index 000000000..5934d9aeb
--- /dev/null
+++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelDialogFragmentTest.kt
@@ -0,0 +1,149 @@
+/* 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.trackingprotection
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.spyk
+import io.mockk.verify
+import junit.framework.TestCase.assertNotSame
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineDispatcher
+import mozilla.components.browser.state.action.ContentAction
+import mozilla.components.browser.state.action.TabListAction
+import mozilla.components.browser.state.action.TrackingProtectionAction.TrackerBlockedAction
+import mozilla.components.browser.state.action.TrackingProtectionAction.TrackerLoadedAction
+import mozilla.components.browser.state.selector.findTab
+import mozilla.components.browser.state.state.TabSessionState
+import mozilla.components.browser.state.state.createTab
+import mozilla.components.browser.state.store.BrowserStore
+import mozilla.components.support.test.ext.joinBlocking
+import mozilla.components.support.test.rule.MainCoroutineRule
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mozilla.fenix.helpers.FenixRobolectricTestRunner
+
+@ExperimentalCoroutinesApi
+@RunWith(FenixRobolectricTestRunner::class)
+class TrackingProtectionPanelDialogFragmentTest {
+
+ private val testDispatcher = TestCoroutineDispatcher()
+
+ @get:Rule
+ val coroutinesTestRule = MainCoroutineRule(testDispatcher)
+ private lateinit var lifecycleOwner: MockedLifecycleOwner
+ private lateinit var fragment: TrackingProtectionPanelDialogFragment
+ private lateinit var store: BrowserStore
+
+ @Before
+ fun setup() {
+ fragment = spyk(TrackingProtectionPanelDialogFragment())
+ lifecycleOwner = MockedLifecycleOwner(Lifecycle.State.STARTED)
+
+ store = BrowserStore()
+ every { fragment.view } returns mockk(relaxed = true)
+ every { fragment.lifecycle } returns lifecycleOwner.lifecycle
+ every { fragment.activity } returns mockk(relaxed = true)
+ }
+
+ @After
+ fun cleanUp() {
+ testDispatcher.cleanupTestCoroutines()
+ }
+
+ @Test
+ fun `WHEN the url is updated THEN the url view is updated`() {
+ val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
+ val tab = createTab("mozilla.org")
+
+ every { fragment.trackingProtectionStore } returns trackingProtectionStore
+ every { fragment.provideTabId() } returns tab.id
+
+ fragment.observeUrlChange(store)
+ addAndSelectTab(tab)
+
+ verify(exactly = 1) {
+ trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("mozilla.org"))
+ }
+
+ store.dispatch(ContentAction.UpdateUrlAction(tab.id, "wikipedia.org")).joinBlocking()
+
+ verify(exactly = 1) {
+ trackingProtectionStore.dispatch(TrackingProtectionAction.UrlChange("wikipedia.org"))
+ }
+ }
+
+ @Test
+ fun `WHEN a tracker is loaded THEN trackers view is updated`() {
+ val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
+ val tab = createTab("mozilla.org")
+
+ every { fragment.trackingProtectionStore } returns trackingProtectionStore
+ every { fragment.provideTabId() } returns tab.id
+ every { fragment.updateTrackers(any()) } returns Unit
+
+ fragment.observeTrackersChange(store)
+ addAndSelectTab(tab)
+
+ verify(exactly = 1) {
+ fragment.updateTrackers(tab)
+ }
+
+ store.dispatch(TrackerLoadedAction(tab.id, mockk())).joinBlocking()
+
+ val updatedTab = store.state.findTab(tab.id)!!
+
+ assertNotSame(updatedTab, tab)
+
+ verify(exactly = 1) {
+ fragment.updateTrackers(updatedTab)
+ }
+ }
+
+ @Test
+ fun `WHEN a tracker is blocked THEN trackers view is updated`() {
+ val trackingProtectionStore: TrackingProtectionStore = mockk(relaxed = true)
+ val tab = createTab("mozilla.org")
+
+ every { fragment.trackingProtectionStore } returns trackingProtectionStore
+ every { fragment.provideTabId() } returns tab.id
+ every { fragment.updateTrackers(any()) } returns Unit
+
+ fragment.observeTrackersChange(store)
+ addAndSelectTab(tab)
+
+ verify(exactly = 1) {
+ fragment.updateTrackers(tab)
+ }
+
+ store.dispatch(TrackerBlockedAction(tab.id, mockk())).joinBlocking()
+
+ val updatedTab = store.state.findTab(tab.id)!!
+
+ assertNotSame(updatedTab, tab)
+
+ verify(exactly = 1) {
+ fragment.updateTrackers(tab)
+ }
+ }
+ private fun addAndSelectTab(tab: TabSessionState) {
+ store.dispatch(TabListAction.AddTabAction(tab)).joinBlocking()
+ store.dispatch(TabListAction.SelectTabAction(tab.id)).joinBlocking()
+ }
+
+ internal class MockedLifecycleOwner(initialState: Lifecycle.State) : LifecycleOwner {
+ private val lifecycleRegistry = LifecycleRegistry(this).apply {
+ currentState = initialState
+ }
+
+ override fun getLifecycle(): Lifecycle = lifecycleRegistry
+ }
+}
diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt
index 08d571ad6..5bfac924b 100644
--- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelInteractorTest.kt
@@ -28,6 +28,22 @@ class TrackingProtectionPanelInteractorTest {
}
}
+ @Test
+ fun openDetailsForRedirectTrackers() {
+ val store: TrackingProtectionStore = mockk(relaxed = true)
+ val interactor =
+ TrackingProtectionPanelInteractor(store, mockk(), mockk())
+ interactor.openDetails(TrackingProtectionCategory.REDIRECT_TRACKERS, true)
+ verify {
+ store.dispatch(
+ TrackingProtectionAction.EnterDetailsMode(
+ TrackingProtectionCategory.REDIRECT_TRACKERS,
+ true
+ )
+ )
+ }
+ }
+
@Test
fun selectTrackingProtectionSettings() {
var openSettings = false
diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt
index eb56f1a02..884a26529 100644
--- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionPanelViewTest.kt
@@ -29,7 +29,7 @@ class TrackingProtectionPanelViewTest {
private lateinit var interactor: TrackingProtectionPanelInteractor
private lateinit var view: TrackingProtectionPanelView
private val baseState = TrackingProtectionState(
- session = null,
+ tab = null,
url = "",
isTrackingProtectionEnabled = false,
listTrackers = emptyList(),
diff --git a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt
index db2e66bc8..208396029 100644
--- a/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/trackingprotection/TrackingProtectionStoreTest.kt
@@ -6,7 +6,7 @@ package org.mozilla.fenix.trackingprotection
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
-import mozilla.components.browser.session.Session
+import mozilla.components.browser.state.state.SessionState
import mozilla.components.concept.engine.content.blocking.TrackerLog
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotSame
@@ -14,7 +14,7 @@ import org.junit.Test
class TrackingProtectionStoreTest {
- val session: Session = mockk(relaxed = true)
+ val tab: SessionState = mockk(relaxed = true)
@Test
fun enterDetailsMode() = runBlocking {
@@ -131,7 +131,7 @@ class TrackingProtectionStoreTest {
}
private fun defaultState(): TrackingProtectionState = TrackingProtectionState(
- session = session,
+ tab = tab,
url = "www.mozilla.org",
isTrackingProtectionEnabled = true,
listTrackers = listOf(),
@@ -140,7 +140,7 @@ class TrackingProtectionStoreTest {
)
private fun detailsState(): TrackingProtectionState = TrackingProtectionState(
- session = session,
+ tab = tab,
url = "www.mozilla.org",
isTrackingProtectionEnabled = true,
listTrackers = listOf(),
diff --git a/app/src/test/java/org/mozilla/fenix/utils/ClipboardHandlerTest.kt b/app/src/test/java/org/mozilla/fenix/utils/ClipboardHandlerTest.kt
index c4eacde88..b70c8e624 100644
--- a/app/src/test/java/org/mozilla/fenix/utils/ClipboardHandlerTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/utils/ClipboardHandlerTest.kt
@@ -7,7 +7,13 @@ package org.mozilla.fenix.utils
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
+import io.mockk.every
+import io.mockk.mockkObject
+import io.mockk.spyk
+import io.mockk.unmockkObject
+import io.mockk.verify
import mozilla.components.support.test.robolectric.testContext
+import mozilla.components.support.utils.SafeUrl
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
@@ -59,4 +65,33 @@ class ClipboardHandlerTest {
clipboard.setPrimaryClip(ClipData.newHtmlText("Html", clipboardUrl, clipboardUrl))
assertEquals(clipboardUrl, clipboardHandler.url)
}
+
+ @Test
+ fun `text should return firstSafePrimaryClipItemText`() {
+ val safeResult = "safeResult"
+ clipboard.setPrimaryClip(ClipData.newPlainText(clipboardUrl, clipboardText))
+ clipboardHandler = spyk(clipboardHandler)
+ every { clipboardHandler getProperty "firstSafePrimaryClipItemText" } propertyType String::class returns safeResult
+
+ val result = clipboardHandler.text
+
+ verify { clipboardHandler getProperty "firstSafePrimaryClipItemText" }
+ assertEquals(safeResult, result)
+ }
+
+ @Test
+ fun `firstSafePrimaryClipItemText should return the result of SafeUrl#stripUnsafeUrlSchemes`() {
+ mockkObject(SafeUrl)
+ try {
+ every { SafeUrl.stripUnsafeUrlSchemes(any(), any()) } returns "safeResult"
+ clipboard.setPrimaryClip(ClipData.newHtmlText("Html", clipboardUrl, clipboardUrl))
+
+ val result = clipboardHandler.firstSafePrimaryClipItemText
+
+ verify { SafeUrl.stripUnsafeUrlSchemes(testContext, clipboardUrl) }
+ assertEquals("safeResult", result)
+ } finally {
+ unmockkObject(SafeUrl)
+ }
+ }
}
diff --git a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
index c14831064..f48fc43ad 100644
--- a/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
+++ b/app/src/test/java/org/mozilla/fenix/utils/SettingsTest.kt
@@ -236,27 +236,28 @@ class SettingsTest {
// When just created
// Then
assertTrue(settings.manuallyCloseTabs)
+ assertEquals(Long.MAX_VALUE, settings.getTabTimeout())
// When
settings.manuallyCloseTabs = false
settings.closeTabsAfterOneDay = true
// Then
- assertEquals(settings.getTabTimeout(), Settings.ONE_DAY_MS)
+ assertEquals(Settings.ONE_DAY_MS, settings.getTabTimeout())
// When
settings.closeTabsAfterOneDay = false
settings.closeTabsAfterOneWeek = true
// Then
- assertEquals(settings.getTabTimeout(), Settings.ONE_WEEK_MS)
+ assertEquals(Settings.ONE_WEEK_MS, settings.getTabTimeout())
// When
settings.closeTabsAfterOneWeek = false
settings.closeTabsAfterOneMonth = true
// Then
- assertEquals(settings.getTabTimeout(), Settings.ONE_MONTH_MS)
+ assertEquals(Settings.ONE_MONTH_MS, settings.getTabTimeout())
}
@Test
@@ -577,7 +578,7 @@ class SettingsTest {
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_AUDIBLE, ALLOWED)
assertEquals(
- defaultPermissions.copy(autoplayAudible = ALLOWED),
+ defaultPermissions.copy(autoplayAudible = AutoplayAction.ALLOWED),
settings.getSitePermissionsCustomSettingsRules()
)
}
@@ -587,7 +588,7 @@ class SettingsTest {
settings.setSitePermissionsPhoneFeatureAction(PhoneFeature.AUTOPLAY_INAUDIBLE, ALLOWED)
assertEquals(
- defaultPermissions.copy(autoplayInaudible = ALLOWED),
+ defaultPermissions.copy(autoplayInaudible = AutoplayAction.ALLOWED),
settings.getSitePermissionsCustomSettingsRules()
)
}
diff --git a/buildSrc/src/main/java/AndroidComponents.kt b/buildSrc/src/main/java/AndroidComponents.kt
index 2b6160546..dbe57e43d 100644
--- a/buildSrc/src/main/java/AndroidComponents.kt
+++ b/buildSrc/src/main/java/AndroidComponents.kt
@@ -3,5 +3,5 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
object AndroidComponents {
- const val VERSION = "70.0.18"
+ const val VERSION = "72.0.16"
}
diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index 582ba81e0..65fd88c4f 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -78,6 +78,7 @@ object Deps {
const val mozilla_browser_icons = "org.mozilla.components:browser-icons:${Versions.mozilla_android_components}"
const val mozilla_browser_search = "org.mozilla.components:browser-search:${Versions.mozilla_android_components}"
const val mozilla_browser_session = "org.mozilla.components:browser-session:${Versions.mozilla_android_components}"
+ const val mozilla_browser_session_storage = "org.mozilla.components:browser-session-storage:${Versions.mozilla_android_components}"
const val mozilla_browser_state = "org.mozilla.components:browser-state:${Versions.mozilla_android_components}"
const val mozilla_browser_tabstray = "org.mozilla.components:browser-tabstray:${Versions.mozilla_android_components}"
const val mozilla_browser_thumbnails = "org.mozilla.components:browser-thumbnails:${Versions.mozilla_android_components}"
diff --git a/docs/metrics.md b/docs/metrics.md
index e54034a39..9d9b9eef3 100644
--- a/docs/metrics.md
+++ b/docs/metrics.md
@@ -85,6 +85,10 @@ The following metrics are added to the ping:
| contextual_hint.tracking_protection.display |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The enhanced tracking protection contextual hint was displayed. |[1](https://github.com/mozilla-mobile/fenix/pull/11923), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 |
| contextual_hint.tracking_protection.inside_tap |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user tapped inside of the etp contextual hint (which brings up the etp panel for this site). |[1](https://github.com/mozilla-mobile/fenix/pull/11923), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 |
| contextual_hint.tracking_protection.outside_tap |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The user tapped outside of the etp contextual hint (which has no effect). |[1](https://github.com/mozilla-mobile/fenix/pull/11923), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 |
+| contextual_menu.copy_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The context menu's 'copy' option was used. |[1](https://github.com/mozilla-mobile/fenix/pull/16968)||2021-06-01 |2 |
+| contextual_menu.search_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The context menu's 'search' option was used. |[1](https://github.com/mozilla-mobile/fenix/pull/16968)||2021-06-01 |2 |
+| contextual_menu.select_all_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The context menu's 'select all' option was used. |[1](https://github.com/mozilla-mobile/fenix/pull/16968)||2021-06-01 |2 |
+| contextual_menu.share_tapped |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The context menu's 'share' option was used. |[1](https://github.com/mozilla-mobile/fenix/pull/16968)||2021-06-01 |2 |
| crash_reporter.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The crash reporter was closed |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)|
crash_submitted: A boolean that tells us whether or not the user submitted a crash report
|2021-04-01 |2 |
| crash_reporter.opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The crash reporter was displayed |[1](https://github.com/mozilla-mobile/fenix/pull/1214#issue-264756708), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 |
| custom_tab.action_button |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the action button provided by the launching app |[1](https://github.com/mozilla-mobile/fenix/pull/1697), [2](https://github.com/mozilla-mobile/fenix/pull/13958#issuecomment-676857877)||2021-04-01 |2 |
@@ -193,6 +197,8 @@ The following metrics are added to the ping:
| tab.media_pause |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the pause icon on a tab from the home screen |[1](https://github.com/mozilla-mobile/fenix/pull/5266), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| tab.media_play |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user pressed the play icon on a tab from the home screen |[1](https://github.com/mozilla-mobile/fenix/pull/5266), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| tabs.setting_opened |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |The tab settings were opened. |[1](https://github.com/mozilla-mobile/fenix/pull/15811#issuecomment-706402952)||2021-08-01 |2 |
+| tabs_tray.cfr.dismiss |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user dismisses the tabs tray CFR. |[1](https://github.com/mozilla-mobile/fenix/pull/17442), [2](https://github.com/mozilla-mobile/fenix/issues/16485#issuecomment-759641324)||2021-08-01 |2 |
+| tabs_tray.cfr.go_to_settings |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user selects the CFR option to navigate to settings. |[1](https://github.com/mozilla-mobile/fenix/pull/17442), [2](https://github.com/mozilla-mobile/fenix/issues/16485#issuecomment-759641324)||2021-08-01 |2 |
| tabs_tray.close_all_tabs |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user tapped the close all tabs button in the three dot menu within the tabs tray |[1](https://github.com/mozilla-mobile/fenix/pull/12036), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| tabs_tray.closed |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user closed the tabs tray |[1](https://github.com/mozilla-mobile/fenix/pull/12036), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| tabs_tray.closed_existing_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user closed an existing tab |[1](https://github.com/mozilla-mobile/fenix/pull/12036), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
@@ -212,6 +218,7 @@ The following metrics are added to the ping:
| top_sites.long_press |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user long pressed on a top site |[1](https://github.com/mozilla-mobile/fenix/pull/15136), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)|
type: The type of top site. Options are: "FRECENCY," "DEFAULT," or "PINNED."
|2021-08-01 |2 |
| top_sites.open_default |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened a default top site |[1](https://github.com/mozilla-mobile/fenix/pull/10752), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| top_sites.open_frecency |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened a frecency top site |[1](https://github.com/mozilla-mobile/fenix/pull/15136), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
+| top_sites.open_google_search_attribution |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened the google top site |[1](https://github.com/mozilla-mobile/fenix/pull/17637)||2021-08-01 |2 |
| top_sites.open_in_new_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opens a new tab based on a top site item |[1](https://github.com/mozilla-mobile/fenix/pull/7523), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| top_sites.open_in_private_tab |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opens a new private tab based on a top site item |[1](https://github.com/mozilla-mobile/fenix/pull/7523), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
| top_sites.open_pinned |[event](https://mozilla.github.io/glean/book/user/metrics/event.html) |A user opened a pinned top site |[1](https://github.com/mozilla-mobile/fenix/pull/15136), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
@@ -288,7 +295,7 @@ The following metrics are added to the ping:
| metrics.mobile_bookmarks_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter that indicates how many bookmarks a user has in the mobile folder. This value will only be set if the user has at least *one* bookmark. If they have 0, this ping will not get sent, resulting in a null value. To disambiguate between a failed `mobile_bookmarks_count` ping and 0 bookmarks, please see `has_mobile_bookmarks`. |[1](https://github.com/mozilla-mobile/fenix/pull/16942), [2](https://github.com/mozilla-mobile/fenix/pull/16942#issuecomment-742794701)||2021-08-01 |2 |
| metrics.mozilla_products |[string_list](https://mozilla.github.io/glean/book/user/metrics/string_list.html) |A list of all the Mozilla products installed on device. We currently scan for: Firefox, Firefox Beta, Firefox Aurora, Firefox Nightly, Firefox Fdroid, Firefox Lite, Reference Browser, Reference Browser Debug, Fenix, Focus, and Lockwise. |[1](https://github.com/mozilla-mobile/fenix/pull/1953/), [2](https://github.com/mozilla-mobile/fenix/pull/5216), [3](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |1, 2 |
| metrics.recently_used_pwa_count |[counter](https://mozilla.github.io/glean/book/user/metrics/counter.html) |A counter that indicates how many PWAs a user has recently used. Threshold for "recency" set in HomeActivity#PWA_RECENTLY_USED_THRESHOLD. Currently we are not told by the OS when a PWA is removed by the user, so we use the "recently used" heuristic to judge how many PWAs are still active, as a proxy for "installed". This value will only be set if the user has at least *one* recently used PWA. If they have 0, this metric will not be sent, resulting in a null value during analysis on the server-side. To disambiguate between a failed `recently_used_pwa_count` metric and 0 recent PWAs, please see `has_recent_pwas`. |[1](https://github.com/mozilla-mobile/fenix/pull/11982#pullrequestreview-437963817), [2](https://github.com/mozilla-mobile/fenix/pull/15713#issuecomment-703972068)||2021-08-01 |2 |
-| metrics.search_count |[labeled_counter](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html) |The labels for this counter are `.